quickjs 0.17.0 → 0.18.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 +4 -4
- data/README.md +102 -3
- data/ext/quickjsrb/quickjsrb.c +357 -25
- data/ext/quickjsrb/quickjsrb.h +39 -7
- data/ext/quickjsrb/vendor/polyfill-intl-en.min.js +4 -4
- data/lib/quickjs/version.rb +1 -1
- data/lib/quickjs.rb +19 -5
- data/polyfills/package-lock.json +110 -110
- data/polyfills/package.json +1 -1
- data/sig/quickjs.rbs +14 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e7a32a1c083c824ccd0a1cbebc2fba793faf1f1fc743e19deaa9f0d7dd5b93cf
|
|
4
|
+
data.tar.gz: 669a202e7818baeae6966241b1cc1c0784de15b16682ac2713795fe8832ee605
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97e22453d9391a9a9056751371652a0561479f46d7bc9ead83d0f65208a09faad7a7d96a9ada99710df66a0790287ce714db3f30fec7ef22ca00f3e8fc93727b
|
|
7
|
+
data.tar.gz: 7292f78c043a8bd20702157e4481d72d9a835b03df76bee1a666796f96479b5e94cba4bcbfde18cdb61e759decb056c07bc94830b58fba6b80568c31cb607d18
|
data/README.md
CHANGED
|
@@ -100,6 +100,15 @@ runnable.run(on: { features: [::Quickjs::POLYFILL_INTL] }) # ad-hoc VM with opti
|
|
|
100
100
|
|
|
101
101
|
`Runnable#to_s` returns the underlying bytecode as a frozen ASCII-8BIT `String`, suitable for caching to memory or disk. `Quickjs::Runnable.new(bytecode_string)` reconstructs a `Runnable` from that blob — validation happens lazily at `run` time, so a corrupt or wrong-build blob surfaces as `Quickjs::RuntimeError` when executed. The bytecode format is tied to the QuickJS build, so include the gem version in your cache key if you persist across upgrades.
|
|
102
102
|
|
|
103
|
+
`Quickjs.compile` is a one-shot convenience that creates and immediately disposes a throwaway VM:
|
|
104
|
+
|
|
105
|
+
```rb
|
|
106
|
+
runnable = Quickjs.compile(File.read('big_bundle.js'), filename: 'big_bundle.js')
|
|
107
|
+
runnable.run # execute on a fresh VM, no parse cost
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Accepts `filename:` and the same VM options as `Quickjs.eval_code` (`memory_limit:`, `timeout_msec:`, etc.) — useful when compiling large bundles that exceed the default limits.
|
|
111
|
+
|
|
103
112
|
#### `Quickjs::VM#call`: ⚡ Call a JS function directly with Ruby arguments
|
|
104
113
|
|
|
105
114
|
```rb
|
|
@@ -176,9 +185,64 @@ vm.import(['a'], filename: 'a')
|
|
|
176
185
|
vm.eval_code('a()') #=> 'a-b-result'
|
|
177
186
|
```
|
|
178
187
|
|
|
179
|
-
The Proc
|
|
188
|
+
The Proc may accept one or two arguments. Single-arity (`->(specifier) { ... }`) is the legacy shape: the Proc gets the raw import specifier and returns the source for it. Two-arity (`->(specifier, importer) { ... }`) additionally receives the file that issued the `import`, which is what you need for importmap-style scoping. Pass `nil` to clear a previously set loader.
|
|
189
|
+
|
|
190
|
+
Return value:
|
|
191
|
+
|
|
192
|
+
- A `String` — that's the source. The canonical name used for QuickJS's module cache is the specifier itself.
|
|
193
|
+
- A `Hash` `{ code:, as: }` — `code:` is the source, `as:` becomes the canonical name. Use this when the same specifier should resolve to different modules depending on the importer (importmap "scopes"), since QuickJS caches by canonical name and changing the canonical is what isolates the two modules.
|
|
194
|
+
- `nil` or `false` — raises `Quickjs::ReferenceError` on the JS side ("module not found").
|
|
195
|
+
- Anything else — `Quickjs::TypeError`.
|
|
196
|
+
|
|
197
|
+
Importmap scope example:
|
|
198
|
+
|
|
199
|
+
```rb
|
|
200
|
+
modules = {
|
|
201
|
+
'/vendor/lodash.js' => 'export default { v: "global" };',
|
|
202
|
+
'/vendor/lodash-admin.js' => 'export default { v: "admin" };',
|
|
203
|
+
'/app/admin/main.js' => "import _ from 'lodash'; export const tag = _.v;"
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
vm.module_loader = ->(specifier, importer) {
|
|
207
|
+
case specifier
|
|
208
|
+
when 'lodash'
|
|
209
|
+
importer.start_with?('/app/admin/') \
|
|
210
|
+
? { code: modules['/vendor/lodash-admin.js'], as: '/vendor/lodash-admin.js' }
|
|
211
|
+
: { code: modules['/vendor/lodash.js'], as: '/vendor/lodash.js' }
|
|
212
|
+
else
|
|
213
|
+
modules[specifier]
|
|
214
|
+
end
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
vm.import(['tag'], filename: '/app/admin/main.js')
|
|
218
|
+
vm.eval_code('tag') #=> 'admin'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Without `as:`, the canonical equals the raw `specifier`. That means relative imports (`./foo.js`) from different importers all share one canonical (`./foo.js`) and therefore one cached module instance — which is rarely what you want. If you support relative imports from a plain `String` return, resolve them to absolute paths yourself before returning, or use the `Hash` form with `as:` set to the resolved path. The user-facing Proc is called at most once per `(specifier, importer)` pair across the VM's lifetime; subsequent imports hit a per-VM resolution cache.
|
|
222
|
+
|
|
223
|
+
When `module_loader=` is set, pass `filename:` to `import` instead of `from:` to resolve a named specifier directly through the loader — no inline bridge source needed. Passing both `from:` and `filename:` raises `ArgumentError`.
|
|
180
224
|
|
|
181
|
-
|
|
225
|
+
`import` awaits the module's top-level evaluation — top-level `await`, synchronous body execution, and any chained dynamic `import()`. The call blocks until the module's settle promise resolves. A top-level throw, a failed dynamic `import()`, or a rejected top-level `await` propagates back to Ruby as the matching `Quickjs::*Error` instead of being silently dropped.
|
|
226
|
+
|
|
227
|
+
#### `Quickjs::VM#on_unhandled_rejection`: 🚨 Catch promise rejections that have no handler
|
|
228
|
+
|
|
229
|
+
Register a block to be notified when a JS Promise rejects with no `.catch` / `then(_, onRejected)` attached at the time of rejection — fire-and-forget chains, failed dynamic imports without `try`, etc.
|
|
230
|
+
|
|
231
|
+
```rb
|
|
232
|
+
vm = Quickjs::VM.new
|
|
233
|
+
vm.on_unhandled_rejection do |err|
|
|
234
|
+
warn "[JS] unhandled rejection: #{err.class} #{err.message}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
vm.eval_code("void Promise.reject(new TypeError('drift'));")
|
|
238
|
+
#=> warns: [JS] unhandled rejection: Quickjs::TypeError drift
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Calling `on_unhandled_rejection` again with a new block replaces the previously registered one (matching `on_log`).
|
|
242
|
+
|
|
243
|
+
The block receives a `Quickjs::*Error` matching the rejection reason (`Quickjs::TypeError` for `new TypeError`, etc.); non-`Error` rejections (`Promise.reject('str')`, `Promise.reject({})`) are wrapped in `Quickjs::RuntimeError`. The exception's `#backtrace` carries the JS-side stack frames (`at func (file:line:col)`) for `Error` rejections, so the rejection site shows up directly when you log or re-raise. Exceptions raised inside the block are swallowed — propagating them out would corrupt the QuickJS runtime.
|
|
244
|
+
|
|
245
|
+
The tracker fires synchronously when QuickJS first observes the rejection. A `.catch` attached later in the same tick does **not** suppress the notification, and a chain like `Promise.reject(x).then(y).then(z)` without a terminating `.catch` may emit a notification per intermediate promise. If that noise is a problem, attach handlers synchronously or dedupe by reason identity in your block. The block runs on the QuickJS stack — heavy work blocks JS execution.
|
|
182
246
|
|
|
183
247
|
#### `Quickjs::VM#define_function`: 💎 Define a global function for JS by Ruby
|
|
184
248
|
|
|
@@ -275,6 +339,42 @@ rescue Quickjs::RuntimeError => e
|
|
|
275
339
|
end
|
|
276
340
|
```
|
|
277
341
|
|
|
342
|
+
#### `Quickjs::VM#dispose!`: 🧹 Release the underlying C-side runtime eagerly
|
|
343
|
+
|
|
344
|
+
By default, the `JSRuntime` / `JSContext` behind a `Quickjs::VM` lives until Ruby's GC reclaims the wrapping object. Ruby's GC sizes its trigger by the Ruby-side object footprint (a few pointers) and doesn't see the C-side JS heap, so a workload that rebuilds VMs frequently — per-request, per-page-visit, throwaway pool — can let several megabytes per dead VM accumulate before a major GC fires.
|
|
345
|
+
|
|
346
|
+
`dispose!` frees the runtime immediately and marks the VM unusable:
|
|
347
|
+
|
|
348
|
+
```rb
|
|
349
|
+
vm = Quickjs::VM.new(features: [::Quickjs::POLYFILL_INTL])
|
|
350
|
+
vm.eval_code('…')
|
|
351
|
+
vm.dispose! # frees JSContext + JSRuntime now
|
|
352
|
+
vm.disposed? #=> true
|
|
353
|
+
vm.eval_code('1 + 1') # raises Quickjs::RuntimeError "VM has been disposed"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`dispose!` is idempotent and safe to call before letting Ruby drop the reference — the dfree handler is a no-op on an already-disposed VM. The teardown itself can take tens of milliseconds on a VM with polyfills loaded; the GVL is released during the free so other Ruby threads (e.g. a background pool builder) keep running. For fire-and-forget teardown that doesn't block the caller, wrap it in a thread:
|
|
357
|
+
|
|
358
|
+
```rb
|
|
359
|
+
Thread.new { vm.dispose! }
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### `Quickjs::VM#drain_jobs!`: Run pending JS jobs to completion
|
|
363
|
+
|
|
364
|
+
QuickJS does not automatically drain the job queue at the end of a synchronous `eval_code` / `call`. Continuations scheduled via `Promise.resolve().then(...)` or `JS_EnqueueJob` stay pending until something explicitly runs them — `await` inside JS does, but a sync return path does not.
|
|
365
|
+
|
|
366
|
+
```rb
|
|
367
|
+
vm = Quickjs::VM.new
|
|
368
|
+
vm.eval_code('globalThis.x = 0; Promise.resolve().then(() => { x = 1 }); void 0')
|
|
369
|
+
vm.eval_code('x') #=> 0 (the .then() callback hasn't run yet)
|
|
370
|
+
vm.drain_jobs! #=> 1 (number of jobs executed)
|
|
371
|
+
vm.eval_code('x') #=> 1
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`drain_jobs!` keeps running until the queue empties, so jobs that schedule further jobs all run in a single call. The drain is bounded by the VM's `timeout_msec`; exceeding it raises `Quickjs::InterruptedError`.
|
|
375
|
+
|
|
376
|
+
Useful when porting JS that assumed V8's implicit-drain semantics — V8 (and therefore [mini_racer](https://github.com/rubyjs/mini_racer)) flushes pending jobs at every eval boundary, so `eval_code` already sees `.then()` continuations run by the time it returns. QuickJS doesn't. Patterns like `Promise.resolve().then(() => { ... })` and Stimulus/Hotwire callbacks that assume "the next microtask tick" silently fall through unless you call `drain_jobs!` explicitly.
|
|
377
|
+
|
|
278
378
|
### Value Conversion
|
|
279
379
|
|
|
280
380
|
| JavaScript | | Ruby | Note |
|
|
@@ -310,7 +410,6 @@ end
|
|
|
310
410
|
- [@formatjs/intl-pluralrules](https://github.com/formatjs/formatjs/blob/main/packages/intl-pluralrules/LICENSE.md)
|
|
311
411
|
- [@formatjs/intl-numberformat](https://github.com/formatjs/formatjs/blob/main/packages/intl-numberformat/LICENSE.md)
|
|
312
412
|
- [@formatjs/intl-datetimeformat](https://github.com/formatjs/formatjs/blob/main/packages/intl-datetimeformat/LICENSE.md)
|
|
313
|
-
- [@formatjs/ecma402-abstract](https://github.com/formatjs/formatjs/blob/main/packages/ecma402-abstract/LICENSE.md)
|
|
314
413
|
- [@formatjs/fast-memoize](https://github.com/formatjs/formatjs/blob/main/packages/fast-memoize/LICENSE.md)
|
|
315
414
|
- [@formatjs/intl-localematcher](https://github.com/formatjs/formatjs/blob/main/packages/intl-localematcher/LICENSE.md)
|
|
316
415
|
- MIT License Copyright (c) 2026 FormatJS
|