Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
- (Unreleased)
- Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks
- Add ES module API via `Context#compile_module` / `MiniRacer::Module` (#421): `compile_module(source, filename:)` returns a Module handle bound to the Context (and exposes `filename` to the module as `import.meta.url`); `Module#instantiate { |specifier, referrer_url| ... }` walks static imports via a Ruby-side resolver block (the referrer URL is the importing module's filename, so relative specifiers can be resolved); `Module#evaluate` runs the module body; `Module#namespace` returns the Module Namespace Object as a Hash; `Module#status` exposes V8's lifecycle state; `Module#dispose` / `Module#disposed?` mirror `Script`'s eager handle release. `Context#dynamic_import_resolver=` handles JS `import(...)` expressions (resolver receives specifier + referrer URL, returns a `MiniRacer::Module`; the loaded module is evaluated if pending). Top-level await is not supported in this round (evaluate raises if the evaluation promise stays pending). The TruffleRuby shim raises `NotImplementedError`.

- 0.21.1 - 25-05-2026
- Run `:single_threaded` V8 dispatches on a reusable mini_racer-owned native thread so V8 does not execute on Ruby-owned threads
Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,77 @@ context.eval("log")

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`.

### ES modules

`Context#compile_module` exposes V8's ES module API for code that uses
`import` / `export` syntax. Unlike `eval` (which only accepts script-level
syntax), modules can have static imports that resolve to other modules and
expose named exports through a real Module Namespace Object.

```ruby
context = MiniRacer::Context.new

dep = context.compile_module("export const base = 10", filename: "dep.js")
main = context.compile_module(<<~JS, filename: "main.js")
import { base } from 'dep'
export const doubled = base * 2
JS

main.instantiate {|specifier, referrer| dep } # called once per static import
dep.evaluate
main.evaluate

main.namespace # => {"doubled" => 20}
```

* `Context#compile_module(source, filename:)` — parses the source as a
module; the returned `MiniRacer::Module` is bound to its Context. The
`filename` is also exposed to the module as `import.meta.url`.
* `Module#instantiate { |specifier, referrer_url| ... }` — walks the static
import graph. The resolver block is called once per import declaration
with the raw specifier string and the importing module's filename, so
relative specifiers (`./foo`, `../bar`) can be resolved against the
referrer. It must return another `MiniRacer::Module` (typically from a
per-Context cache). Imports can also be resolved lazily from inside the
block via further `Context#compile_module` calls.
* `Module#evaluate` — runs the module body. Returns the evaluation result
(`nil` for the typical `export const …` shape). Modules with top-level
`await` raise `MiniRacer::RuntimeError` for now.
* `Module#namespace` — returns the Module Namespace Object as a Hash
(`{ "default" => …, "namedExport" => … }`). Available after
`instantiate` succeeds; `evaluate` populates the values.
* `Module#status` — one of `:uninstantiated`, `:instantiating`,
`:instantiated`, `:evaluating`, `:evaluated`, `:errored`.
* `Module#dispose` / `Module#disposed?` — eager handle release, mirroring
the convention used elsewhere.
* `Context#dynamic_import_resolver = proc { |specifier, referrer_url| ... }`
— handler for JS `import(...)` expressions. The proc must return a
`MiniRacer::Module` (already instantiated; `evaluate` is driven for you
if pending). Set to `nil` to reject all dynamic imports. Drain the
microtask queue with `Context#perform_microtask_checkpoint` to see the
result in a `.then` callback or after `await`.

```ruby
context.dynamic_import_resolver = ->(spec, _ref) { cache.fetch(spec) }
context.eval(%(import('dep').then(ns => globalThis.r = ns.x)), filename: 'caller.js')
context.perform_microtask_checkpoint
context.eval('globalThis.r') # => 42
```

Notes:

- A `Module` is bound to the `Context` that compiled it; resolvers must
return modules from the same Context.
- `Module#dispose` frees the underlying V8 handle eagerly. The Ruby GC
finalizer does not (taking the V8 lock from a finalizer thread risks
deadlock), so long-lived Contexts with many short-lived modules
accumulate handles until `Context#dispose` clears them.
- Top-level await is not yet supported; `evaluate` raises if the
module's evaluation promise stays pending after the microtask drain.
- On TruffleRuby, `Context#compile_module` raises `NotImplementedError`
— GraalJS has its own module-loading mechanism that doesn't map onto
this handle-based API. PRs to bridge are welcome.

## Performance

The `bench` folder contains benchmark.
Expand Down
Loading
Loading