mini_racer-csim 0.21.1.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.
data/README.md ADDED
@@ -0,0 +1,687 @@
1
+ # MiniRacer
2
+
3
+ [![Test](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml/badge.svg)](https://github.com/rubyjs/mini_racer/actions/workflows/ci.yml) ![Gem](https://img.shields.io/gem/v/mini_racer)
4
+
5
+ Minimal, modern embedded V8 for Ruby.
6
+
7
+ MiniRacer provides a minimal two way bridge between the V8 JavaScript engine and Ruby.
8
+
9
+ It was created as an alternative to the excellent [therubyracer](https://github.com/cowboyd/therubyracer), which is [no longer maintained](https://github.com/rubyjs/therubyracer/issues/462). Unlike therubyracer, mini_racer only implements a minimal bridge. This reduces the surface area making upgrading v8 much simpler and exhaustive testing simpler.
10
+
11
+ MiniRacer has an adapter for [execjs](https://github.com/rails/execjs) so it can be used directly with Rails projects to minify assets, run babel or compile CoffeeScript.
12
+
13
+ ## This repository is `mini_racer-csim` (a fork)
14
+
15
+ This is **`mini_racer-csim`**, a private fork of [`mini_racer`](https://github.com/rubyjs/mini_racer) maintained for [capybara-simulated](https://github.com/ursm/capybara-simulated). It adds browser-fidelity extensions (ES modules, realm reset, …) that capybara-simulated needs but most users do not — **if you are not using capybara-simulated, use upstream `mini_racer`.**
16
+
17
+ It stays a **drop-in replacement**: the gem is still loaded with `require "mini_racer"` and exposes the `MiniRacer` module, so existing code keeps working. Only the gem name (`mini_racer-csim`) differs.
18
+
19
+ ### Additions over upstream
20
+
21
+ | Feature | API | Notes |
22
+ | --- | --- | --- |
23
+ | Bytecode cache | `Context#compile(src, cached_data:, produce_cache:)` → `Script`, `Script#run`, `Script#cache_rejected?` | Cross-process V8 bytecode caching to skip parsing; see [Bytecode cache for repeated script evaluation](#bytecode-cache-for-repeated-script-evaluation) below |
24
+ | ES Module API | `Context#compile_module` → `MiniRacer::Module` (`#instantiate` / `#evaluate` / `#namespace` / `#status` / `#cached_data` / `#dispose`); `Context#dynamic_import_resolver=` | V8's ES module pipeline, `import.meta.url`, dynamic `import()` |
25
+ | Batched module-graph loader | `Context#load_module_graph(resolve:, …)` | Loads an ESM graph in one batched, native (C++) pass; one `Module` per URL shared across every load path |
26
+ | Realm reset | `Context#reset_realm` | Discards the user realm (`globalThis`) while keeping the warm isolate (browser per-navigation model); re-binds attached host functions and the host namespace |
27
+ | Host namespace | `Context.new(host_namespace: "MiniRacer")` → `globalThis.MiniRacer.drainMicrotasks()` | Opt-in JS namespace exposing an inline, rendezvous-free microtask checkpoint |
28
+ | GVL release on boot | (automatic) | Releases the Ruby GVL while the V8 thread boots the isolate |
29
+
30
+ The fork is periodically rebased on upstream `mini_racer` to pick up V8 / `libv8-node` bumps and bug fixes.
31
+
32
+ ## Supported Ruby Versions & Troubleshooting
33
+
34
+ MiniRacer only supports non-EOL versions of Ruby. See [Ruby Maintenance Branches](https://www.ruby-lang.org/en/downloads/branches/) for the list of non-EOL Rubies. If you require support for older versions of Ruby install an older version of the gem. [TruffleRuby](https://github.com/oracle/truffleruby) is also supported.
35
+
36
+ MiniRacer **does not support**
37
+
38
+ * [Ruby built on MinGW](https://github.com/rubyjs/mini_racer/issues/252#issuecomment-1201172236), "pure windows" no Cygwin, no WSL2 (see https://github.com/rubyjs/libv8-node/issues/9)
39
+ * [JRuby](https://www.jruby.org)
40
+
41
+ If you have a problem installing MiniRacer, please consider the following steps:
42
+
43
+ * make sure you try the latest released version of `mini_racer`
44
+ * make sure you have Rubygems >= 3.2.13 and bundler >= 2.2.13 installed: `gem update --system`
45
+ * if you are using bundler
46
+ * make sure it is actually using the latest bundler version: [`bundle update --bundler`](https://bundler.io/v2.4/man/bundle-update.1.html)
47
+ * make sure to have `PLATFORMS` set correctly in `Gemfile.lock` via [`bundle lock --add-platform`](https://bundler.io/v2.4/man/bundle-lock.1.html#SUPPORTING-OTHER-PLATFORMS)
48
+ * make sure to recompile/reinstall `mini_racer` and `libv8-node` after OS upgrades (for example via `gem uninstall --all mini_racer libv8-node`)
49
+ * make sure you are on the latest patch/teeny version of a supported Ruby branch
50
+
51
+ ## Features
52
+
53
+ ### Simple eval for JavaScript
54
+
55
+ You can simply eval one or many JavaScript snippets in a shared context
56
+
57
+ ```ruby
58
+ context = MiniRacer::Context.new
59
+ context.eval("var adder = (a,b)=>a+b;")
60
+ puts context.eval("adder(20,22)")
61
+ # => 42
62
+ ```
63
+
64
+ ### Attach global Ruby functions to your JavaScript context
65
+
66
+ You can attach one or many ruby proc that can be accessed via JavaScript
67
+
68
+ ```ruby
69
+ context = MiniRacer::Context.new
70
+ context.attach("math.adder", proc{|a,b| a+b})
71
+ puts context.eval("math.adder(20,22)")
72
+ # => 42
73
+ ```
74
+
75
+ ```ruby
76
+ context = MiniRacer::Context.new
77
+ context.attach("array_and_hash", proc{{a: 1, b: [1, {a: 1}]}})
78
+ puts context.eval("array_and_hash()")
79
+ # => {"a" => 1, "b" => [1, {"a" => 1}]}
80
+ ```
81
+
82
+ ### Return binary data from Ruby to JavaScript
83
+
84
+ Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`:
85
+
86
+ ```ruby
87
+ require "digest"
88
+
89
+ context = MiniRacer::Context.new
90
+ context.attach("sha256_raw", ->(data) {
91
+ MiniRacer::Binary.new(Digest::SHA256.digest(data))
92
+ })
93
+
94
+ # Inside JavaScript the return value is a Uint8Array
95
+ context.eval("sha256_raw('hello') instanceof Uint8Array") # => true
96
+ context.eval("sha256_raw('hello').length") # => 32
97
+ ```
98
+
99
+ This is useful when you need to pass raw bytes (e.g., cryptographic digests, compressed data, binary file contents) from Ruby to JavaScript. The `MiniRacer::Binary` wrapper tells the bridge to serialize the data as a `Uint8Array` on the JavaScript side rather than a string.
100
+
101
+ ### GIL free JavaScript execution
102
+
103
+ The Ruby Global interpreter lock is released when scripts are executing:
104
+
105
+ ```ruby
106
+ context = MiniRacer::Context.new
107
+ Thread.new do
108
+ sleep 1
109
+ context.stop
110
+ end
111
+ context.eval("while(true){}")
112
+ # => exception is raised
113
+ ```
114
+
115
+ This allows you to execute multiple scripts in parallel.
116
+
117
+ ### Timeout Support
118
+
119
+ Contexts can specify a default timeout for scripts
120
+
121
+ ```ruby
122
+ context = MiniRacer::Context.new(timeout: 1000)
123
+ context.eval("while(true){}")
124
+ # => exception is raised after 1 second (1000 ms)
125
+ ```
126
+
127
+ ### Memory softlimit Support
128
+
129
+ Contexts can specify a memory softlimit for scripts
130
+
131
+ ```ruby
132
+ # terminates script if heap usage exceeds 200mb after V8 garbage collection has run
133
+ context = MiniRacer::Context.new(max_memory: 200_000_000)
134
+ context.eval("var a = new Array(10000); while(true) {a = a.concat(new Array(10000)) }")
135
+ # => V8OutOfMemoryError is raised
136
+ ```
137
+
138
+ ### Rich Debugging with File Name in Stack Trace Support
139
+
140
+ You can provide `filename:` to `#eval` which will be used in stack traces produced by V8:
141
+
142
+ ```ruby
143
+ context = MiniRacer::Context.new
144
+ context.eval("var foo = function() {bar();}", filename: "a/foo.js")
145
+ context.eval("bar()", filename: "a/bar.js")
146
+
147
+ # JavaScript at a/bar.js:1:1: ReferenceError: bar is not defined (MiniRacer::RuntimeError)
148
+ # …
149
+ ```
150
+
151
+ ### Bytecode cache for repeated script evaluation
152
+
153
+ `Context#compile` returns a `MiniRacer::Script` handle you can run multiple times,
154
+ and exposes V8's bytecode cache so subsequent Contexts can skip the parse step.
155
+
156
+ In a single process — e.g. warming a `Context` pool from one canonical compile:
157
+
158
+ ```ruby
159
+ # Warm the cache once — top-level compile, opt in with produce_cache: true.
160
+ warm = MiniRacer::Context.new
161
+ warmed = warm.compile(File.read("bundle.js"),
162
+ filename: "bundle.js",
163
+ produce_cache: true)
164
+ warmed.run
165
+ blob = warmed.cached_data # ASCII-8BIT String, hold onto it in memory
166
+
167
+ # Subsequent Contexts (e.g. a per-request pool) consume the blob and skip parsing.
168
+ ctx = MiniRacer::Context.new
169
+ script = ctx.compile(File.read("bundle.js"),
170
+ filename: "bundle.js",
171
+ cached_data: blob)
172
+ # script.cache_rejected? is false when V8 accepted the blob.
173
+ script.run
174
+ ```
175
+
176
+ Across processes (e.g. persisting blobs to disk), the consumer must boot from
177
+ **byte-identical snapshot data** — two separate `Snapshot.new(src)` calls produce
178
+ different blobs even for the same `src`, and V8 will then reject every cached
179
+ blob. Use `Snapshot#dump` / `Snapshot.load` to share canonical bytes:
180
+
181
+ ```ruby
182
+ # Build the snapshot once, persist its bytes.
183
+ snap_bytes = MiniRacer::Snapshot.new(snapshot_src).dump
184
+ File.binwrite("snapshot.bin", snap_bytes)
185
+
186
+ # Every process loads the same bytes.
187
+ snap = MiniRacer::Snapshot.load(File.binread("snapshot.bin"))
188
+ ctx = MiniRacer::Context.new(snapshot: snap)
189
+ script = ctx.compile(File.read("bundle.js"),
190
+ filename: "bundle.js",
191
+ cached_data: File.binread("bundle.js.cache"))
192
+ script.run
193
+ ```
194
+
195
+ `produce_cache` defaults to `false`; pass `true` to ask V8 for the cache blob.
196
+ When the supplied `cached_data` is accepted, `script.cached_data` returns `nil` so
197
+ callers can skip a redundant copy. When V8 produces a fresh blob (initial compile
198
+ with `produce_cache: true`, or a rejection while `produce_cache: true` was also
199
+ set), it returns the new bytes.
200
+
201
+ `MiniRacer::V8_CACHED_DATA_VERSION_TAG` exposes V8's
202
+ `ScriptCompiler::CachedDataVersionTag()` — mix it into your cache key alongside
203
+ the source hash so a libv8-node version bump invalidates stale blobs automatically.
204
+ The constant is populated on first `Context.new` (after `Platform.set_flags!`),
205
+ so read it after constructing at least one Context.
206
+
207
+ ```ruby
208
+ key = "#{Digest::SHA256.hexdigest(source)}-#{MiniRacer::V8_CACHED_DATA_VERSION_TAG}"
209
+ ```
210
+
211
+ Notes:
212
+
213
+ - A `Script` is bound to the `Context` that compiled it; reusing it on another
214
+ Context isn't supported.
215
+ - `Script#dispose` frees the underlying V8 handle eagerly. The Ruby GC finalizer
216
+ does not (taking the V8 lock from a finalizer thread risks deadlock), so
217
+ long-lived Contexts with many short-lived scripts accumulate handles until
218
+ `Context#dispose` clears them.
219
+ - `produce_cache: true` is only safe at the top level. From inside a host-fn
220
+ callback (i.e., re-entrant compile while a JS → Ruby → JS frame is on the
221
+ stack) it raises `MiniRacer::RuntimeError`, because V8's `CreateCodeCache`
222
+ walks live isolate state and corrupts the parser when re-entered. Warm the
223
+ cache from the top level once and pass it back via `cached_data:` from your
224
+ callbacks.
225
+ - Cross-process reuse is **incompatible with `MiniRacer::Platform.set_flags!(:single_threaded)`**.
226
+ V8's single-threaded mode embeds process-local state in the cache blob, so
227
+ every cached_data is rejected when consumed in a fresh process. Same-process
228
+ reuse still works under `:single_threaded`. If you need both cross-process
229
+ reuse and `:single_threaded` (e.g. for fork-safety reasons), disable
230
+ `:single_threaded` for the path that produces / consumes the cache.
231
+ - On TruffleRuby, `Script` is implemented as source replay (GraalJS has no
232
+ equivalent per-script bytecode cache reachable from `Polyglot::InnerContext`),
233
+ so `cached_data` and `produce_cache` are silently ignored and `cached_data`
234
+ always returns `nil`, and `MiniRacer::V8_CACHED_DATA_VERSION_TAG` is `0`.
235
+
236
+ ### Fork Safety
237
+
238
+ Some Ruby web servers employ forking (for example unicorn or puma in clustered mode). V8 is not fork safe by default and sadly Ruby does not have support for fork notifications per [#5446](https://bugs.ruby-lang.org/issues/5446).
239
+
240
+ Since 0.6.1 mini_racer does support V8 single threaded platform mode which should remove most forking related issues. To enable run this before using `MiniRacer::Context`, for example in a Rails initializer:
241
+
242
+ ```ruby
243
+ MiniRacer::Platform.set_flags!(:single_threaded)
244
+ ```
245
+
246
+ When using pre-fork `MiniRacer::Context` objects in `:single_threaded` mode,
247
+ ensure the process only forks while MiniRacer is quiescent: no thread may be
248
+ evaluating JavaScript, calling into a context, disposing/freeing a context,
249
+ running a Ruby callback from JavaScript, or otherwise using MiniRacer at the
250
+ instant of `fork`. In multi-threaded applications, guard all MiniRacer context
251
+ operations and the `fork` itself with the same application-level lock. Forking
252
+ while a MiniRacer operation is in progress can leave inherited pthread mutexes
253
+ in an unusable state in the child process.
254
+
255
+ If you want to ensure your application does not leak memory after fork either:
256
+
257
+ 1. Ensure no `MiniRacer::Context` objects are created in the master process; or
258
+ 2. Dispose manually of all `MiniRacer::Context` objects prior to forking
259
+
260
+ ```ruby
261
+ # before fork
262
+
263
+ require "objspace"
264
+ ObjectSpace.each_object(MiniRacer::Context){|c| c.dispose}
265
+
266
+ # fork here
267
+ ```
268
+
269
+ ### Threadsafe
270
+
271
+ Context usage is threadsafe
272
+
273
+ ```ruby
274
+ context = MiniRacer::Context.new
275
+ context.eval("counter=0; plus=()=>counter++;")
276
+
277
+ (1..10).map do
278
+ Thread.new {
279
+ context.eval("plus()")
280
+ }
281
+ end.each(&:join)
282
+
283
+ puts context.eval("counter")
284
+ # => 10
285
+ ```
286
+
287
+ ### Snapshots
288
+
289
+ Contexts can be created with pre-loaded snapshots:
290
+
291
+ ```ruby
292
+ snapshot = MiniRacer::Snapshot.new("function hello() { return 'world!'; }")
293
+
294
+ context = MiniRacer::Context.new(snapshot: snapshot)
295
+
296
+ context.eval("hello()")
297
+ # => "world!"
298
+ ```
299
+
300
+ Snapshots can come in handy for example if you want your contexts to be pre-loaded for efficiency. It uses [V8 snapshots](http://v8project.blogspot.com/2015/09/custom-startup-snapshots.html) under the hood; see [this link](http://v8project.blogspot.com/2015/09/custom-startup-snapshots.html) for caveats using these, in particular:
301
+
302
+ > There is an important limitation to snapshots: they can only capture V8’s
303
+ > heap. Any interaction from V8 with the outside is off-limits when creating the
304
+ > snapshot. Such interactions include:
305
+ >
306
+ > * defining and calling API callbacks (i.e. functions created via v8::FunctionTemplate)
307
+ > * creating typed arrays, since the backing store may be allocated outside of V8
308
+ >
309
+ > And of course, values derived from sources such as `Math.random` or `Date.now`
310
+ > are fixed once the snapshot has been captured. They are no longer really random
311
+ > nor reflect the current time.
312
+
313
+ Also note that snapshots can be warmed up, using the `warmup!` method, which allows you to call functions which are otherwise lazily compiled to get them to compile right away; any side effect of your warm up code being then dismissed. [More details on warming up here](https://github.com/electron/electron/issues/169#issuecomment-76783481), and a small example:
314
+
315
+ ```ruby
316
+ snapshot = MiniRacer::Snapshot.new("var counter = 0; function hello() { counter++; return 'world! '; }")
317
+
318
+ snapshot.warmup!("hello()")
319
+
320
+ context = MiniRacer::Context.new(snapshot: snapshot)
321
+
322
+ context.eval("hello()")
323
+ # => "world! 1"
324
+ context.eval("counter")
325
+ # => 1
326
+ ```
327
+
328
+ Snapshots can also be persisted to disk for faster startup:
329
+
330
+ ```ruby
331
+ # Save a snapshot to disk
332
+ snapshot = MiniRacer::Snapshot.new('var foo = "bar";')
333
+ File.binwrite("snapshot.bin", snapshot.dump)
334
+
335
+ # Load it back in a later process
336
+ blob = File.binread("snapshot.bin")
337
+ snapshot = MiniRacer::Snapshot.load(blob)
338
+ context = MiniRacer::Context.new(snapshot: snapshot)
339
+ context.eval("foo")
340
+ # => "bar"
341
+ ```
342
+
343
+ Note that snapshots are architecture and V8-version specific. A snapshot created on one platform (e.g., ARM64 macOS) cannot be loaded on a different platform (e.g., x86_64 Linux). Snapshots are best used for same-machine caching or homogeneous deployment environments.
344
+
345
+ **Security note:** Only load snapshots from trusted sources. V8 snapshots are not designed to be safely loaded from untrusted input—malformed or malicious snapshot data may cause crashes or memory corruption.
346
+
347
+ ### Garbage collection
348
+
349
+ You can make the garbage collector more aggressive by defining the context with `MiniRacer::Context.new(ensure_gc_after_idle: 1000)`. Using this will ensure V8 will run a full GC using `context.low_memory_notification` 1 second after the last eval on the context. Low memory notifications ensure long living contexts use minimal amounts of memory.
350
+
351
+ ### V8 Runtime flags
352
+
353
+ It is possible to set V8 Runtime flags:
354
+
355
+ ```ruby
356
+ MiniRacer::Platform.set_flags! :noconcurrent_recompilation, max_inlining_levels: 10
357
+ ```
358
+
359
+ This can come in handy if you want to use MiniRacer with Unicorn, which doesn't seem to always appreciate V8's liberal use of threading:
360
+
361
+ ```ruby
362
+ MiniRacer::Platform.set_flags! :noconcurrent_recompilation, :noconcurrent_sweeping
363
+ ```
364
+
365
+ Or else to unlock experimental features in V8, for example tail recursion optimization:
366
+
367
+ ```ruby
368
+ MiniRacer::Platform.set_flags! :harmony
369
+
370
+ js = <<-JS
371
+ 'use strict';
372
+ var f = function f(n){
373
+ if (n <= 0) {
374
+ return 'foo';
375
+ }
376
+ return f(n - 1);
377
+ }
378
+
379
+ f(1e6);
380
+ JS
381
+
382
+ context = MiniRacer::Context.new
383
+
384
+ context.eval js
385
+ # => "foo"
386
+ ```
387
+
388
+ The same code without the harmony runtime flag results in a `MiniRacer::RuntimeError: RangeError: Maximum call stack size exceeded` exception.
389
+ Please refer to http://node.green/ as a reference on other harmony features.
390
+
391
+ A list of all V8 runtime flags can be found using `node --v8-options`, or else by perusing [the V8 source code for flags (make sure to use the right version of V8)](https://github.com/v8/v8/blob/master/src/flags/flag-definitions.h).
392
+
393
+ Note that runtime flags must be set before any other operation (e.g. creating a context or a snapshot), otherwise an exception will be thrown.
394
+
395
+ Flags:
396
+
397
+ * `:expose_gc`: Will expose `gc()` which you can run in JavaScript to issue a GC run.
398
+ * `:max_old_space_size`: defaults to 1400 (megs) on 64 bit, you can restrict memory usage by limiting this.
399
+
400
+ **NOTE TO READER** our documentation could be awesome we could be properly documenting all the flags, they are hugely useful, if you feel like documenting a few more, PLEASE DO, PRs are welcome.
401
+
402
+ ## Controlling memory
403
+
404
+ When hosting v8 you may want to keep track of memory usage, use `#heap_stats` to get memory usage:
405
+
406
+ ```ruby
407
+ context = MiniRacer::Context.new
408
+ # use context
409
+ p context.heap_stats
410
+ # {:total_physical_size=>1280640,
411
+ # :total_heap_size_executable=>4194304,
412
+ # :total_heap_size=>3100672,
413
+ # :used_heap_size=>1205376,
414
+ # :heap_size_limit=>1501560832}
415
+ ```
416
+
417
+ If you wish to dispose of a context before waiting on the GC use `#dispose`:
418
+
419
+ ```ruby
420
+ context = MiniRacer::Context.new
421
+ context.eval("let a='testing';")
422
+ context.dispose
423
+ context.eval("a = 2")
424
+ # MiniRacer::ContextDisposedError
425
+
426
+ # nothing works on the context from now on, it's a shell waiting to be disposed
427
+ ```
428
+
429
+ A MiniRacer context can also be dumped in a heapsnapshot file using `#write_heap_snapshot(file_or_io)`
430
+
431
+ ```ruby
432
+ context = MiniRacer::Context.new
433
+ # use context
434
+ context.write_heap_snapshot("test.heapsnapshot")
435
+ ```
436
+
437
+ This file can then be loaded in the "memory" tab of the [Chrome DevTools](https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots/#view_snapshots).
438
+
439
+ ### Function call
440
+
441
+ This calls the function passed as first argument:
442
+
443
+ ```ruby
444
+ context = MiniRacer::Context.new
445
+ context.eval("function hello(name) { return `Hello, ${name}!` }")
446
+ context.call("hello", "George")
447
+ # "Hello, George!"
448
+ ```
449
+
450
+ Performance is slightly better than running `context.eval("hello('George')")` since:
451
+
452
+ * compilation of eval'd string is avoided
453
+ * function arguments don't need to be converted to JSON
454
+
455
+ ### Microtask checkpoints
456
+
457
+ V8 drains its microtask queue (e.g. callbacks queued via `Promise.resolve().then(...)`) automatically when script execution returns to the embedder, so most code "just works":
458
+
459
+ ```ruby
460
+ context = MiniRacer::Context.new
461
+ context.eval(<<~JS)
462
+ let x = 0;
463
+ Promise.resolve().then(() => x = 99);
464
+ JS
465
+ context.eval("x")
466
+ # => 99
467
+ ```
468
+
469
+ When JavaScript invokes a Ruby callback synchronously and you need queued microtasks to drain mid-execution — e.g. for spec-compliant ordering across a chain of synchronous `dispatchEvent` listeners — call `context.perform_microtask_checkpoint` from the callback:
470
+
471
+ ```ruby
472
+ context = MiniRacer::Context.new
473
+ context.attach("drain", -> { context.perform_microtask_checkpoint })
474
+ context.eval(<<~JS)
475
+ globalThis.log = [];
476
+ Promise.resolve().then(() => log.push("microtask"));
477
+ log.push("before");
478
+ drain();
479
+ log.push("after");
480
+ JS
481
+ context.eval("log")
482
+ # => ["before", "microtask", "after"]
483
+ ```
484
+
485
+ Without `drain()` the order would be `["before", "after", "microtask"]` because the microtask only runs once the outermost script returns. `perform_microtask_checkpoint` is a thin wrapper over V8's `MicrotasksScope::PerformCheckpoint`.
486
+
487
+ When the drain has to happen from within JavaScript itself — for example between each listener in a synchronous `dispatchEvent` chain — the same checkpoint is available to JS as `drainMicrotasks()`. It runs inline on the V8 thread without the Ruby ↔ V8 round-trip, so no `attach` is required.
488
+
489
+ It is exposed through an opt-in **host namespace** — a single object (in the spirit of Deno's `Deno` or Bun's `Bun`) that mini_racer hangs its non-standard helpers off. Pass `host_namespace:` to enable it; by default nothing is injected and the global stays clean:
490
+
491
+ ```ruby
492
+ context = MiniRacer::Context.new(host_namespace: "MiniRacer")
493
+ context.eval(<<~JS)
494
+ globalThis.log = [];
495
+ Promise.resolve().then(() => log.push("microtask"));
496
+ log.push("before");
497
+ MiniRacer.drainMicrotasks();
498
+ log.push("after");
499
+ JS
500
+ context.eval("log")
501
+ # => ["before", "microtask", "after"]
502
+ ```
503
+
504
+ `host_namespace:` accepts a String (the global name to use — it must be a valid JavaScript identifier), `true` (the default name `"MiniRacer"`), or `nil`/`false` (the default — inject nothing). The namespace object is defined non-enumerable so it does not appear in `Object.keys(globalThis)`, while its methods are ordinary properties discoverable via `Object.keys(MiniRacer)`. Like `perform_microtask_checkpoint`, `drainMicrotasks()` is a no-op while a microtask checkpoint is already in progress, and it lets watchdog/out-of-memory termination propagate to the enclosing `eval`/`call`. (The host namespace is V8-only; it is not installed on the TruffleRuby backend.)
505
+
506
+ ### ES modules
507
+
508
+ `Context#compile_module` exposes V8's ES module API for code that uses
509
+ `import` / `export` syntax. Unlike `eval` (which only accepts script-level
510
+ syntax), modules can have static imports that resolve to other modules and
511
+ expose named exports through a real Module Namespace Object.
512
+
513
+ ```ruby
514
+ context = MiniRacer::Context.new
515
+
516
+ dep = context.compile_module("export const base = 10", filename: "dep.js")
517
+ main = context.compile_module(<<~JS, filename: "main.js")
518
+ import { base } from 'dep'
519
+ export const doubled = base * 2
520
+ JS
521
+
522
+ main.instantiate {|specifier, referrer| dep } # called once per static import
523
+ dep.evaluate
524
+ main.evaluate
525
+
526
+ main.namespace # => {"doubled" => 20}
527
+ ```
528
+
529
+ * `Context#compile_module(source, filename:)` — parses the source as a
530
+ module; the returned `MiniRacer::Module` is bound to its Context. The
531
+ `filename` is also exposed to the module as `import.meta.url`.
532
+ * `Module#instantiate { |specifier, referrer_url| ... }` — walks the static
533
+ import graph. The resolver block is called once per import declaration
534
+ with the raw specifier string and the importing module's filename, so
535
+ relative specifiers (`./foo`, `../bar`) can be resolved against the
536
+ referrer. It must return another `MiniRacer::Module` (typically from a
537
+ per-Context cache). Imports can also be resolved lazily from inside the
538
+ block via further `Context#compile_module` calls.
539
+ * `Module#evaluate` — runs the module body. Returns the evaluation result
540
+ (`nil` for the typical `export const …` shape). Modules with top-level
541
+ `await` raise `MiniRacer::RuntimeError` for now.
542
+ * `Module#namespace` — returns the Module Namespace Object as a Hash
543
+ (`{ "default" => …, "namedExport" => … }`). Available after
544
+ `instantiate` succeeds; `evaluate` populates the values.
545
+ * `Module#status` — one of `:uninstantiated`, `:instantiating`,
546
+ `:instantiated`, `:evaluating`, `:evaluated`, `:errored`.
547
+ * `Module#dispose` / `Module#disposed?` — eager handle release, mirroring
548
+ the convention used elsewhere.
549
+ * `Context#dynamic_import_resolver = proc { |specifier, referrer_url| ... }`
550
+ — handler for JS `import(...)` expressions. The proc must return a
551
+ `MiniRacer::Module` (already instantiated; `evaluate` is driven for you
552
+ if pending). Set to `nil` to reject all dynamic imports. Drain the
553
+ microtask queue with `Context#perform_microtask_checkpoint` to see the
554
+ result in a `.then` callback or after `await`.
555
+
556
+ ```ruby
557
+ context.dynamic_import_resolver = ->(spec, _ref) { cache.fetch(spec) }
558
+ context.eval(%(import('dep').then(ns => globalThis.r = ns.x)), filename: 'caller.js')
559
+ context.perform_microtask_checkpoint
560
+ context.eval('globalThis.r') # => 42
561
+ ```
562
+
563
+ Notes:
564
+
565
+ - A `Module` is bound to the `Context` that compiled it; resolvers must
566
+ return modules from the same Context.
567
+ - `Module#dispose` frees the underlying V8 handle eagerly. The Ruby GC
568
+ finalizer does not (taking the V8 lock from a finalizer thread risks
569
+ deadlock), so long-lived Contexts with many short-lived modules
570
+ accumulate handles until `Context#dispose` clears them.
571
+ - Top-level await is not yet supported; `evaluate` raises if the
572
+ module's evaluation promise stays pending after the microtask drain.
573
+ - On TruffleRuby, `Context#compile_module` raises `NotImplementedError`
574
+ — GraalJS has its own module-loading mechanism that doesn't map onto
575
+ this handle-based API. PRs to bridge are welcome.
576
+
577
+ ## Performance
578
+
579
+ The `bench` folder contains benchmark.
580
+
581
+ ### Benchmark minification of Discourse application.js (both minified and non-minified)
582
+
583
+ MiniRacer outperforms node when minifying assets via execjs.
584
+
585
+ * MiniRacer version 0.1.9
586
+ * node version 6.10
587
+ * therubyracer version 0.12.2
588
+
589
+ ```terminal
590
+ $ bundle exec ruby bench.rb mini_racer
591
+ Benching with mini_racer
592
+ mini_racer minify discourse_app.js 9292.72063ms
593
+ mini_racer minify discourse_app_minified.js 11799.850171ms
594
+ mini_racer minify discourse_app.js twice (2 threads) 10269.570797ms
595
+
596
+ sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb node
597
+ Benching with node
598
+ node minify discourse_app.js 13302.715484ms
599
+ node minify discourse_app_minified.js 18100.761243ms
600
+ node minify discourse_app.js twice (2 threads) 14383.600207000001ms
601
+
602
+ sam@ubuntu exec_js_uglify % bundle exec ruby bench.rb therubyracer
603
+ Benching with therubyracer
604
+ therubyracer minify discourse_app.js 171683.01867700001ms
605
+ therubyracer minify discourse_app_minified.js 143138.88492ms
606
+ therubyracer minify discourse_app.js twice (2 threads) NEVER FINISH
607
+
608
+ Killed: 9
609
+ ```
610
+
611
+ The huge performance disparity (MiniRacer is 10x faster) is due to MiniRacer running latest version of V8. In July 2016 there is a queued upgrade to therubyracer which should bring some of the perf inline.
612
+
613
+ Note how the global interpreter lock release leads to 2 threads doing the same work taking the same wall time as 1 thread.
614
+
615
+ As a rule MiniRacer strives to always support and depend on the latest stable version of libv8.
616
+
617
+ ## Source Maps
618
+
619
+ MiniRacer can fully support source maps but must be configured correctly to do so. [Check out this example](./examples/source-map-support/) for a working implementation.
620
+
621
+ ## Installation
622
+
623
+ Add this line to your application's Gemfile:
624
+
625
+ ```ruby
626
+ gem "mini_racer"
627
+ ```
628
+
629
+ And then execute:
630
+
631
+ ```terminal
632
+ $ bundle
633
+
634
+ Or install it yourself as:
635
+
636
+ ```terminal
637
+ $ gem install mini_racer
638
+ ```
639
+
640
+ **Note** using v8.h and compiling MiniRacer requires a C++20 capable compiler.
641
+ gcc >= 12.2 and Xcode >= 13 are, at the time of writing, known to work.
642
+
643
+ ## Similar Projects
644
+
645
+ ### therubyracer
646
+
647
+ * https://github.com/cowboyd/therubyracer
648
+ * Most comprehensive bridge available
649
+ * Provides the ability to "eval" JavaScript
650
+ * Provides the ability to invoke Ruby code from JavaScript
651
+ * Hold references to JavaScript objects and methods in your Ruby code
652
+ * Hold references to Ruby objects and methods in JavaScript code
653
+ * Uses libv8, so installation is fast
654
+ * Supports timeouts for JavaScript execution
655
+ * Does not release global interpreter lock, so performance is constrained to a single thread
656
+ * Currently (May 2016) only supports v8 version 3.16.14 (Released approx November 2013), plans to upgrade by July 2016
657
+ * Supports execjs
658
+
659
+ ### v8eval
660
+
661
+ * https://github.com/sony/v8eval
662
+ * Provides the ability to "eval" JavaScript using the latest V8 engine
663
+ * Does not depend on the [libv8](https://github.com/cowboyd/libv8) gem, installation can take 10-20 mins as V8 needs to be downloaded and compiled.
664
+ * Does not release global interpreter lock when executing JavaScript
665
+ * Does not allow you to invoke Ruby code from JavaScript
666
+ * Multi runtime support due to SWIG based bindings
667
+ * Supports a JavaScript debugger
668
+ * Does not support timeouts for JavaScript execution
669
+ * No support for execjs (can not be used with Rails uglifier and coffeescript gems)
670
+
671
+ ### therubyrhino
672
+
673
+ * https://github.com/cowboyd/therubyrhino
674
+ * API compatible with therubyracer
675
+ * Uses Mozilla's Rhino engine https://github.com/mozilla/rhino
676
+ * Requires JRuby
677
+ * Support for timeouts for JavaScript execution
678
+ * Concurrent cause .... JRuby
679
+ * Supports execjs
680
+
681
+ ## Contributing
682
+
683
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rubyjs/mini_racer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
684
+
685
+ ## License
686
+
687
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).