quickjs 0.18.0 → 0.19.0.pre1

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: e7a32a1c083c824ccd0a1cbebc2fba793faf1f1fc743e19deaa9f0d7dd5b93cf
4
- data.tar.gz: 669a202e7818baeae6966241b1cc1c0784de15b16682ac2713795fe8832ee605
3
+ metadata.gz: 40b6432886ae2f54e4b38466b0916b7cdf94ff2e7c3ac94f6f3b66ebd762f09a
4
+ data.tar.gz: 4720748aaec1473b7c5a9a9ea2348196f45bbe9ac27ed6009a467d7e37ae0761
5
5
  SHA512:
6
- metadata.gz: 97e22453d9391a9a9056751371652a0561479f46d7bc9ead83d0f65208a09faad7a7d96a9ada99710df66a0790287ce714db3f30fec7ef22ca00f3e8fc93727b
7
- data.tar.gz: 7292f78c043a8bd20702157e4481d72d9a835b03df76bee1a666796f96479b5e94cba4bcbfde18cdb61e759decb056c07bc94830b58fba6b80568c31cb607d18
6
+ metadata.gz: 97aa1ca89e4bc79c12fe11f228202426d3b95ffa2558a6803f1045e58a983f7d0abb3b597185182bf1e507c5dc48ccecf237596c70feb1d21a2f862a64635472
7
+ data.tar.gz: 5c0932632f6f619fd7cb720b3ae6f0444ca20fd08413af42fa235bf6b5741e872e8e5139fe9a38df6c9810e8a50e5653e7da56ae1ff303134a1833e6521a034d
data/README.md CHANGED
@@ -392,6 +392,25 @@ Useful when porting JS that assumed V8's implicit-drain semantics — V8 (and th
392
392
  | `File` | → | `Quickjs::File` — `.name`, `.last_modified` + Blob attrs | requires `POLYFILL_FILE` |
393
393
  | `File` proxy | ← | `::File` | requires `POLYFILL_FILE`; applies to `define_function` return values |
394
394
 
395
+ ## Extending: registering polyfills
396
+
397
+ `Quickjs.register_polyfill(name, source:, init: nil)` adds a polyfill to a process-wide registry. Any VM constructed with `name` in its `features:` list runs the registered bundle on top of the JS runtime. Companion gems use this hook to ship additional polyfills (e.g. `Intl.Collator`, `DisplayNames`) without bundling them into the main gem.
398
+
399
+ ```rb
400
+ Quickjs.register_polyfill(
401
+ :polyfill_my_thing,
402
+ source: File.read('vendor/my-polyfill.min.js'),
403
+ init: 'globalThis.MyThing ||= {};' # optional, runs before the bundle
404
+ )
405
+
406
+ vm = Quickjs::VM.new(features: [:polyfill_my_thing])
407
+ vm.eval_code('MyThing.greet("hi")')
408
+ ```
409
+
410
+ The first VM with a given polyfill pays the parse cost (the source is compiled to QuickJS bytecode on a disposable VM with a generous timeout); subsequent VMs reuse the cached bytecode. The polyfill body runs without consuming the user VM's `timeout_msec` budget — that's reserved for user code.
411
+
412
+ The bundled `POLYFILL_INTL` (FormatJS Intl, `en` locale) is itself registered through this API at gem load time.
413
+
395
414
  ## Acknowledgements
396
415
 
397
416
  - [@ursm](https://github.com/ursm) — for continuous contributions improving performance and developer experience
@@ -401,7 +420,7 @@ Useful when porting JS that assumed V8's implicit-drain semantics — V8 (and th
401
420
 
402
421
  - `ext/quickjsrb/quickjs`
403
422
  - [MIT License Copyright (c) 2017-2021 by Fabrice Bellard and Charlie Gordon](https://github.com/bellard/quickjs/blob/6e2e68fd0896957f92eb6c242a2e048c1ef3cae0/LICENSE).
404
- - `ext/quickjsrb/vendor/polyfill-intl-en.min.js` ([bundled and minified from `polyfills/`](https://github.com/hmsk/quickjs.rb/tree/main/polyfills))
423
+ - `lib/quickjs/polyfills/intl-en.min.js` ([bundled and minified from `polyfills/`](https://github.com/hmsk/quickjs.rb/tree/main/polyfills))
405
424
  - MIT License Copyright (c) 2022 FormatJS
406
425
  - [@formatjs/intl-supportedvaluesof](https://github.com/formatjs/formatjs/blob/main/packages/intl-supportedvaluesof/LICENSE.md)
407
426
  - MIT License Copyright (c) 2023 FormatJS
@@ -12,7 +12,6 @@ $srcs = [
12
12
  'cutils.c',
13
13
  'quickjs.c',
14
14
  'quickjs-libc.c',
15
- 'polyfill-intl-en.min.c',
16
15
  'polyfill-file.min.c',
17
16
  'polyfill-encoding.min.c',
18
17
  'polyfill-url.min.c',
@@ -62,10 +61,6 @@ POLYFILL_OPTS=-fno-string-normalize -fno-typedarray -fno-typedarray -fno-eval -f
62
61
 
63
62
  qjsc: ./qjsc.o $(QJS_LIB_OBJS)
64
63
  $(CC) -g -o $@ $^ -lm -ldl -lpthread
65
- polyfill-intl-en.min.js:
66
- $(COPY) $(srcdir)/vendor/$@ $@
67
- polyfill-intl-en.min.c: ./qjsc polyfill-intl-en.min.js
68
- ./qjsc $(POLYFILL_OPTS) -c -M polyfill/intl-en.so,intlen -m -o $@ polyfill-intl-en.min.js
69
64
  polyfill-file.min.js:
70
65
  $(COPY) $(srcdir)/vendor/$@ $@
71
66
  polyfill-file.min.c: ./qjsc polyfill-file.min.js
@@ -1077,15 +1077,8 @@ static VALUE vm_m_initialize(int argc, VALUE *argv, VALUE r_self)
1077
1077
  JS_NewCFunction(data->context, js_quickjsrb_set_timeout, "setTimeout", 2));
1078
1078
  }
1079
1079
 
1080
- if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillIntlId))))
1081
- {
1082
- const char *defineIntl = "Object.defineProperty(globalThis, 'Intl', { value:{} });\n";
1083
- JSValue j_defineIntl = JS_Eval(data->context, defineIntl, strlen(defineIntl), vmInternalFilename, JS_EVAL_TYPE_GLOBAL);
1084
- JS_FreeValue(data->context, j_defineIntl);
1085
-
1086
- JSValue j_polyfillIntlResult = load_polyfill_bytecode(data->context, &qjsc_polyfill_intl_en_min, qjsc_polyfill_intl_en_min_size);
1087
- JS_FreeValue(data->context, j_polyfillIntlResult);
1088
- }
1080
+ // POLYFILL_INTL is registered Ruby-side via Quickjs.register_polyfill and
1081
+ // applied by the wrapper around VM#initialize in lib/quickjs/polyfills.rb.
1089
1082
 
1090
1083
  if (RTEST(rb_funcall(r_features, rb_intern("include?"), 1, QUICKJSRB_SYM(featurePolyfillFileId))))
1091
1084
  {
@@ -1317,6 +1310,36 @@ static VALUE vm_m_evalBytecode(VALUE r_self, VALUE r_bytecode)
1317
1310
  return to_rb_return_value(data->context, j_returnedValue);
1318
1311
  }
1319
1312
 
1313
+ // Loads pre-compiled polyfill bytecode without arming the eval timer.
1314
+ // The user's `timeout_msec` is a budget for *their* code; running our
1315
+ // own polyfill (~140 ms for FormatJS Intl) under that budget would
1316
+ // interrupt the load on tight defaults. Unlike load_polyfill_bytecode
1317
+ // above we hold the GVL through JS_ReadObject + JS_EvalFunction: the
1318
+ // bytecode buffer is a Ruby String, so releasing would let GC compact
1319
+ // the backing storage out from under us. The static-symbol path can
1320
+ // release safely; this path cannot.
1321
+ static VALUE vm_m_loadPolyfillBytecode(VALUE r_self, VALUE r_bytecode)
1322
+ {
1323
+ VMData *data;
1324
+ TypedData_Get_Struct(r_self, VMData, &vm_type, data);
1325
+
1326
+ check_disposed(data);
1327
+ StringValue(r_bytecode);
1328
+
1329
+ JSValue j_func = JS_ReadObject(data->context,
1330
+ (const uint8_t *)RSTRING_PTR(r_bytecode),
1331
+ (size_t)RSTRING_LEN(r_bytecode),
1332
+ JS_READ_OBJ_BYTECODE);
1333
+ if (JS_IsException(j_func))
1334
+ return to_rb_value(data->context, j_func); // raises
1335
+
1336
+ JSValue j_result = JS_EvalFunction(data->context, j_func); // frees j_func
1337
+ if (JS_IsException(j_result))
1338
+ return to_rb_value(data->context, j_result); // raises
1339
+ JS_FreeValue(data->context, j_result);
1340
+ return Qnil;
1341
+ }
1342
+
1320
1343
  static VALUE vm_m_defineGlobalFunction(int argc, VALUE *argv, VALUE r_self)
1321
1344
  {
1322
1345
  rb_need_block();
@@ -1728,6 +1751,7 @@ RUBY_FUNC_EXPORTED void Init_quickjsrb(void)
1728
1751
  rb_define_method(r_class_vm, "eval_code", vm_m_evalCode, -1);
1729
1752
  rb_define_private_method(r_class_vm, "_compile_to_bytecode", vm_m_compile, -1);
1730
1753
  rb_define_private_method(r_class_vm, "_run_bytecode", vm_m_evalBytecode, 1);
1754
+ rb_define_private_method(r_class_vm, "_load_polyfill_bytecode", vm_m_loadPolyfillBytecode, 1);
1731
1755
  rb_define_method(r_class_vm, "call", vm_m_callGlobalFunction, -1);
1732
1756
  rb_define_method(r_class_vm, "define_function", vm_m_defineGlobalFunction, -1);
1733
1757
  rb_define_method(r_class_vm, "import", vm_m_import, -1);
@@ -14,8 +14,6 @@
14
14
  #include <string.h>
15
15
  #include <time.h>
16
16
 
17
- extern const uint32_t qjsc_polyfill_intl_en_min_size;
18
- extern const uint8_t qjsc_polyfill_intl_en_min;
19
17
  extern const uint32_t qjsc_polyfill_file_min_size;
20
18
  extern const uint8_t qjsc_polyfill_file_min;
21
19
  extern const uint32_t qjsc_polyfill_encoding_min_size;
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # FormatJS Intl polyfill (`en` locale only — getCanonicalLocales, Locale,
4
+ # PluralRules, NumberFormat, DateTimeFormat). Compiled and cached on first
5
+ # VM that enables the feature; the file is not read until then.
6
+
7
+ Quickjs.register_polyfill(
8
+ Quickjs::POLYFILL_INTL,
9
+ source: -> { File.read(File.expand_path('intl-en.min.js', __dir__)) },
10
+ init: "Object.defineProperty(globalThis, 'Intl', { value:{} });"
11
+ )
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quickjs
4
+ # Process-wide registry; entries are lazily compiled to bytecode on
5
+ # first use and the bytecode is reused across VMs.
6
+ @_polyfills = {}
7
+
8
+ # `source:` accepts either a `String` (eager) or a `Proc` returning one
9
+ # (lazy). The lazy form lets a companion gem call `register_polyfill`
10
+ # at require time without paying the file-read cost unless a VM
11
+ # actually opts into the feature.
12
+ def self.register_polyfill(name, source:, init: nil)
13
+ raise ::TypeError, "name must be a Symbol, got #{name.class}" unless name.is_a?(Symbol)
14
+ raise ::TypeError, "source: must be a String or Proc, got #{source.class}" unless source.is_a?(String) || source.is_a?(Proc)
15
+ raise ::TypeError, "init: must be a String or nil, got #{init.class}" unless init.nil? || init.is_a?(String)
16
+
17
+ @_polyfills[name] = {source: source, init: init&.freeze, bytecode: nil}
18
+ nil
19
+ end
20
+
21
+ def self._polyfill_for(name)
22
+ @_polyfills[name]
23
+ end
24
+
25
+ def self._unregister_polyfill(name)
26
+ @_polyfills.delete(name)
27
+ nil
28
+ end
29
+
30
+ def self._apply_registered_polyfills(vm, features)
31
+ features.each do |feature|
32
+ next unless (entry = @_polyfills[feature])
33
+ # `||=` isn't atomic — `_precompile_polyfill` releases the GVL inside
34
+ # `Quickjs.compile`, so two threads racing to construct VMs with the
35
+ # same polyfill can both see nil and both compile. The bytecode write
36
+ # is harmless because both threads produce identical bytes; the only
37
+ # observable cost is wasted compile work, and (for `source: Proc`) a
38
+ # second Proc invocation — so register Procs that are safe to call
39
+ # more than once.
40
+ entry[:bytecode] ||= _precompile_polyfill(entry, feature)
41
+ vm.send(:_load_polyfill_bytecode, entry[:bytecode])
42
+ end
43
+ end
44
+
45
+ # Compiled once per process per polyfill on a disposable VM whose
46
+ # generous timeout covers parsing multi-MB bundles (FormatJS Intl is
47
+ # ~2 MB). The user's per-VM `timeout_msec` is for their own JS — it
48
+ # would otherwise interrupt our infrastructure on tight defaults.
49
+ # `features: []` skips applying any registered polyfills to the temp
50
+ # VM (no recursion / no wasted polyfill loads).
51
+ def self._precompile_polyfill(entry, feature)
52
+ source = entry[:source]
53
+ source = source.call if source.is_a?(Proc)
54
+ combined = entry[:init] ? "#{entry[:init]}\n#{source}" : source
55
+ Quickjs.compile(combined, filename: feature.to_s, timeout_msec: 60_000, features: []).to_s
56
+ end
57
+
58
+ module PolyfillLoader
59
+ def initialize(features: [], **opts)
60
+ super
61
+ Quickjs._apply_registered_polyfills(self, features)
62
+ end
63
+ end
64
+
65
+ # Guard against double-prepend if this file is reloaded (e.g. Rails dev).
66
+ VM.prepend(PolyfillLoader) unless VM.ancestors.include?(PolyfillLoader)
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quickjs
4
- VERSION = "0.18.0"
4
+ VERSION = "0.19.0.pre1"
5
5
  end
data/lib/quickjs.rb CHANGED
@@ -8,6 +8,10 @@ require_relative "quickjs/crypto_key"
8
8
  require_relative "quickjs/function"
9
9
  require_relative "quickjs/quickjsrb"
10
10
  require_relative "quickjs/runnable"
11
+ # Polyfills.rb defines Quickjs.register_polyfill; polyfills/intl.rb calls
12
+ # it at load time, so the order matters.
13
+ require_relative "quickjs/polyfills"
14
+ require_relative "quickjs/polyfills/intl"
11
15
 
12
16
  module Quickjs
13
17
  class Blob
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "quickjs-rb-polyfills",
3
- "version": "0.18.0",
3
+ "version": "0.19.0.pre1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "quickjs-rb-polyfills",
9
- "version": "0.18.0",
9
+ "version": "0.19.0.pre1",
10
10
  "dependencies": {
11
11
  "@formatjs/intl-datetimeformat": "^7.3.1",
12
12
  "@formatjs/intl-getcanonicallocales": "^3.2.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickjs-rb-polyfills",
3
- "version": "0.18.0",
3
+ "version": "0.19.0.pre1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "rolldown -c rolldown.config.mjs",
@@ -4,7 +4,10 @@ export default [
4
4
  defineConfig({
5
5
  input: "src/intl-en.js",
6
6
  output: {
7
- file: "../ext/quickjsrb/vendor/polyfill-intl-en.min.js",
7
+ // Loaded at runtime by lib/quickjs/polyfills/intl.rb via
8
+ // Quickjs.register_polyfill; other entries are still embedded as
9
+ // pre-compiled bytecode in the .so until they go the same way.
10
+ file: "../lib/quickjs/polyfills/intl-en.min.js",
8
11
  format: "iife",
9
12
  minify: true,
10
13
  },
data/sig/quickjs.rbs CHANGED
@@ -14,6 +14,8 @@ module Quickjs
14
14
  def self.eval_code: (String code, ?Hash[Symbol, untyped] overwrite_opts) -> untyped
15
15
  def self.compile: (String source, ?filename: String, **untyped) -> Quickjs::Runnable
16
16
 
17
+ def self.register_polyfill: (Symbol name, source: String | ^() -> String, ?init: String?) -> nil
18
+
17
19
  class Value
18
20
  UNDEFINED: Symbol
19
21
  NAN: Symbol
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quickjs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - hmsk
@@ -86,11 +86,13 @@ files:
86
86
  - ext/quickjsrb/quickjsrb_file.h
87
87
  - ext/quickjsrb/vendor/polyfill-encoding.min.js
88
88
  - ext/quickjsrb/vendor/polyfill-file.min.js
89
- - ext/quickjsrb/vendor/polyfill-intl-en.min.js
90
89
  - ext/quickjsrb/vendor/polyfill-url.min.js
91
90
  - lib/quickjs.rb
92
91
  - lib/quickjs/crypto_key.rb
93
92
  - lib/quickjs/function.rb
93
+ - lib/quickjs/polyfills.rb
94
+ - lib/quickjs/polyfills/intl-en.min.js
95
+ - lib/quickjs/polyfills/intl.rb
94
96
  - lib/quickjs/runnable.rb
95
97
  - lib/quickjs/subtle_crypto.rb
96
98
  - lib/quickjs/version.rb