@salve-software/react-native-nitro-jsdom 1.1.0 → 2.0.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.
Files changed (35) hide show
  1. package/android/CMakeLists.txt +4 -0
  2. package/cpp/HybridHtmlSandbox.cpp +7 -3
  3. package/cpp/lexbor/LexborDocument.cpp +62 -0
  4. package/cpp/lexbor/LexborDocument.hpp +14 -0
  5. package/cpp/quickjs/DOMBindings.cpp +9 -1
  6. package/cpp/quickjs/DOMBindingsInternal.cpp +67 -0
  7. package/cpp/quickjs/DOMBindingsInternal.hpp +39 -0
  8. package/cpp/quickjs/QuickJSRuntime.cpp +13 -0
  9. package/cpp/quickjs/QuickJSRuntime.hpp +28 -0
  10. package/cpp/quickjs/bindings/AbortBindings.cpp +14 -0
  11. package/cpp/quickjs/bindings/BlobBindings.cpp +28 -1
  12. package/cpp/quickjs/bindings/BlobBindings.hpp +3 -3
  13. package/cpp/quickjs/bindings/CSSOMBindings.cpp +46 -0
  14. package/cpp/quickjs/bindings/CSSOMBindings.hpp +3 -1
  15. package/cpp/quickjs/bindings/DOMParserBindings.cpp +299 -0
  16. package/cpp/quickjs/bindings/DOMParserBindings.hpp +45 -0
  17. package/cpp/quickjs/bindings/DocumentBindings.cpp +166 -11
  18. package/cpp/quickjs/bindings/ElementBindings.cpp +296 -20
  19. package/cpp/quickjs/bindings/EventBindings.cpp +277 -5
  20. package/cpp/quickjs/bindings/EventBindings.hpp +3 -0
  21. package/cpp/quickjs/bindings/EventTargetBindings.cpp +75 -0
  22. package/cpp/quickjs/bindings/EventTargetBindings.hpp +24 -0
  23. package/cpp/quickjs/bindings/FetchBindings.cpp +43 -8
  24. package/cpp/quickjs/bindings/FormBindings.cpp +223 -0
  25. package/cpp/quickjs/bindings/FormBindings.hpp +9 -5
  26. package/cpp/quickjs/bindings/LayoutStubBindings.cpp +104 -0
  27. package/cpp/quickjs/bindings/LayoutStubBindings.hpp +28 -0
  28. package/cpp/quickjs/bindings/LiveCollectionBindings.cpp +45 -0
  29. package/cpp/quickjs/bindings/TimerBindings.cpp +79 -0
  30. package/cpp/quickjs/bindings/TreeWalkerBindings.cpp +306 -0
  31. package/cpp/quickjs/bindings/TreeWalkerBindings.hpp +28 -0
  32. package/cpp/quickjs/bindings/UrlBindings.cpp +9 -0
  33. package/cpp/quickjs/bindings/WindowBindings.cpp +286 -4
  34. package/cpp/quickjs/bindings/WindowBindings.hpp +3 -2
  35. package/package.json +17 -2
@@ -1,10 +1,11 @@
1
1
  #include "WindowBindings.hpp"
2
2
  #include "DOMExceptionBindings.hpp"
3
+ #include "EventBindings.hpp"
3
4
  #include "../DOMBindingsInternal.hpp"
4
5
  #include "../QuickJSRuntime.hpp"
6
+ #include <cstdlib>
5
7
  #include <cstring>
6
8
  #include <optional>
7
- #include <random>
8
9
  #include <string>
9
10
  #include <vector>
10
11
 
@@ -191,6 +192,23 @@ JSValue js_console_method(JSContext* ctx, JSValue, int argc, JSValue* argv, int
191
192
  return JS_UNDEFINED;
192
193
  }
193
194
 
195
+ JSValue js_window_reportError(JSContext* ctx, JSValue, int argc, JSValue* argv) {
196
+ JSValue error_value = argc >= 1 ? argv[0] : JS_UNDEFINED;
197
+ std::string message = "Uncaught";
198
+ if (argc >= 1) {
199
+ JSValue msg_prop = JS_GetPropertyStr(ctx, argv[0], "message");
200
+ if (!JS_IsException(msg_prop) && !JS_IsUndefined(msg_prop)) {
201
+ const char* s = JS_ToCString(ctx, msg_prop);
202
+ if (s) { message = s; JS_FreeCString(ctx, s); }
203
+ } else if (JS_IsException(msg_prop)) {
204
+ JS_FreeValue(ctx, JS_GetException(ctx));
205
+ }
206
+ JS_FreeValue(ctx, msg_prop);
207
+ }
208
+ EventBindings::dispatchErrorEvent(ctx, message, error_value);
209
+ return JS_UNDEFINED;
210
+ }
211
+
194
212
  // ── window.alert / confirm / prompt ───────────────────────────────────────────
195
213
 
196
214
  JSValue js_window_alert(JSContext* ctx, JSValue, int argc, JSValue* argv) {
@@ -346,12 +364,12 @@ JSValue js_native_random_bytes(JSContext* ctx, JSValue, int argc, JSValue* argv)
346
364
  if (argc >= 1) JS_ToInt32(ctx, &count, argv[0]);
347
365
  if (count < 0) count = 0;
348
366
 
349
- static thread_local std::mt19937 gen{std::random_device{}()};
350
- std::uniform_int_distribution<int> dist(0, 255);
367
+ std::vector<uint8_t> bytes(static_cast<size_t>(count));
368
+ if (count > 0) arc4random_buf(bytes.data(), bytes.size());
351
369
 
352
370
  JSValue arr = JS_NewArray(ctx);
353
371
  for (int32_t i = 0; i < count; i++) {
354
- JS_SetPropertyUint32(ctx, arr, static_cast<uint32_t>(i), JS_NewInt32(ctx, dist(gen)));
372
+ JS_SetPropertyUint32(ctx, arr, static_cast<uint32_t>(i), JS_NewInt32(ctx, bytes[static_cast<size_t>(i)]));
355
373
  }
356
374
  return arr;
357
375
  }
@@ -373,6 +391,67 @@ const char* kCryptoBootstrapScript = R"JS(
373
391
  for (var i = 0; i < bytes.length; i++) view[i] = bytes[i];
374
392
  return typedArray;
375
393
  };
394
+ globalThis.crypto.randomUUID = function() {
395
+ var bytes = __nativeRandomBytes(16);
396
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
397
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
398
+ var hex = [];
399
+ for (var i = 0; i < 16; i++) {
400
+ var h = bytes[i].toString(16);
401
+ hex.push(h.length === 1 ? '0' + h : h);
402
+ }
403
+ return hex.slice(0, 4).join('') + '-' + hex.slice(4, 6).join('') + '-' +
404
+ hex.slice(6, 8).join('') + '-' + hex.slice(8, 10).join('') + '-' + hex.slice(10, 16).join('');
405
+ };
406
+ })();
407
+ )JS";
408
+
409
+ // ── console ergonomics (group/table/assert/trace/count) ──────────────────────
410
+ // Layered in pure JS on top of the native log/warn/error/info/debug methods
411
+ // above rather than adding new native levels — none of these need anything
412
+ // the onConsole callback can't already receive as stringified args.
413
+
414
+ const char* kConsoleExtrasBootstrapScript = R"JS(
415
+ (function() {
416
+ var counts = Object.create(null);
417
+
418
+ console.group = function() {
419
+ console.log.apply(console, arguments);
420
+ };
421
+ console.groupCollapsed = console.group;
422
+ console.groupEnd = function() {};
423
+
424
+ console.trace = function() {
425
+ var args = Array.prototype.slice.call(arguments);
426
+ args.unshift('Trace:');
427
+ console.log.apply(console, args);
428
+ };
429
+
430
+ console.assert = function(condition) {
431
+ if (condition) return;
432
+ var args = Array.prototype.slice.call(arguments, 1);
433
+ args.unshift('Assertion failed:');
434
+ console.error.apply(console, args);
435
+ };
436
+
437
+ console.table = function(data) {
438
+ try {
439
+ console.log(JSON.stringify(data));
440
+ } catch (e) {
441
+ console.log(String(data));
442
+ }
443
+ };
444
+
445
+ console.count = function(label) {
446
+ label = label === undefined ? 'default' : String(label);
447
+ counts[label] = (counts[label] || 0) + 1;
448
+ console.log(label + ': ' + counts[label]);
449
+ };
450
+
451
+ console.countReset = function(label) {
452
+ label = label === undefined ? 'default' : String(label);
453
+ counts[label] = 0;
454
+ };
376
455
  })();
377
456
  )JS";
378
457
 
@@ -389,10 +468,184 @@ const char* kNavigatorBootstrapScript = R"JS(
389
468
  cookieEnabled: false,
390
469
  hardwareConcurrency: 1,
391
470
  };
471
+ navigator.sendBeacon = function(url, data) {
472
+ try {
473
+ fetch(url, { method: 'POST', body: data }).catch(function() {});
474
+ } catch (e) {
475
+ return false;
476
+ }
477
+ return true;
478
+ };
479
+
480
+ // jsdom itself doesn't implement the Clipboard API at all (no OS clipboard
481
+ // to back it, same reasoning as window.history/getSelection). This is an
482
+ // in-memory stand-in purely so 'copy discount code'-style embedded widgets
483
+ // that call navigator.clipboard.writeText() directly don't throw — reads
484
+ // back whatever was last written, nothing more.
485
+ var clipboardText = '';
486
+ navigator.clipboard = {
487
+ writeText: function(text) {
488
+ clipboardText = String(text);
489
+ return Promise.resolve();
490
+ },
491
+ readText: function() {
492
+ return Promise.resolve(clipboardText);
493
+ },
494
+ };
495
+
392
496
  globalThis.navigator = navigator;
393
497
  })();
394
498
  )JS";
395
499
 
500
+ // ── structuredClone ────────────────────────────────────────────────────────
501
+
502
+ const char* kStructuredCloneBootstrapScript = R"JS(
503
+ (function() {
504
+ globalThis.structuredClone = function(value) {
505
+ var seen = new Map();
506
+ function clone(v) {
507
+ if (v === null || typeof v !== 'object') {
508
+ if (typeof v === 'function' || typeof v === 'symbol') {
509
+ throw new DOMException(String(v) + ' could not be cloned.', 'DataCloneError');
510
+ }
511
+ return v;
512
+ }
513
+ if (seen.has(v)) return seen.get(v);
514
+ if (v instanceof Date) return new Date(v.getTime());
515
+ if (v instanceof RegExp) return new RegExp(v.source, v.flags);
516
+ if (Array.isArray(v)) {
517
+ var arr = [];
518
+ seen.set(v, arr);
519
+ for (var i = 0; i < v.length; i++) arr.push(clone(v[i]));
520
+ return arr;
521
+ }
522
+ if (v instanceof Map) {
523
+ var m = new Map();
524
+ seen.set(v, m);
525
+ v.forEach(function(val, key) { m.set(clone(key), clone(val)); });
526
+ return m;
527
+ }
528
+ if (v instanceof Set) {
529
+ var s = new Set();
530
+ seen.set(v, s);
531
+ v.forEach(function(val) { s.add(clone(val)); });
532
+ return s;
533
+ }
534
+ if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView && ArrayBuffer.isView(v)) {
535
+ return new v.constructor(v);
536
+ }
537
+ if (typeof ArrayBuffer !== 'undefined' && v instanceof ArrayBuffer) {
538
+ return v.slice(0);
539
+ }
540
+ var proto = Object.getPrototypeOf(v);
541
+ if (proto !== Object.prototype && proto !== null) {
542
+ throw new DOMException('The object could not be cloned.', 'DataCloneError');
543
+ }
544
+ var out = {};
545
+ seen.set(v, out);
546
+ for (var key in v) {
547
+ if (Object.prototype.hasOwnProperty.call(v, key)) out[key] = clone(v[key]);
548
+ }
549
+ return out;
550
+ }
551
+ return clone(value);
552
+ };
553
+ })();
554
+ )JS";
555
+
556
+ // ── window.history ────────────────────────────────────────────────────────
557
+ // No real page navigation exists in this sandbox, so History tracks its own
558
+ // in-memory entry stack rather than session history. pushState/replaceState
559
+ // never fire popstate (per spec); go/back/forward do, since those are the
560
+ // events CMS-widget routers actually listen for. `title` is accepted but
561
+ // ignored (same as jsdom — nothing renders a document title). State is
562
+ // structuredClone()'d on write (per spec — matches pushState/replaceState's
563
+ // documented "serializable" semantics), so callers mutating their object
564
+ // after the call can't leak into a stored entry. `go()`'s delta is truncated
565
+ // to an integer so a fractional/NaN input can't produce a non-integer
566
+ // `_index`.
567
+
568
+ const char* kHistoryBootstrapScript = R"JS(
569
+ (function() {
570
+ function History() {
571
+ this._entries = [null];
572
+ this._index = 0;
573
+ }
574
+ Object.defineProperty(History.prototype, 'length', {
575
+ get: function() { return this._entries.length; },
576
+ enumerable: true,
577
+ });
578
+ Object.defineProperty(History.prototype, 'state', {
579
+ get: function() { return this._entries[this._index]; },
580
+ enumerable: true,
581
+ });
582
+ function cloneState(state) {
583
+ return state === undefined ? null : structuredClone(state);
584
+ }
585
+ History.prototype.pushState = function(state, _title, url) {
586
+ this._entries = this._entries.slice(0, this._index + 1);
587
+ this._entries.push(cloneState(state));
588
+ this._index++;
589
+ if (url !== undefined && url !== null) location.href = url;
590
+ };
591
+ History.prototype.replaceState = function(state, _title, url) {
592
+ this._entries[this._index] = cloneState(state);
593
+ if (url !== undefined && url !== null) location.href = url;
594
+ };
595
+ History.prototype.go = function(delta) {
596
+ delta = delta === undefined ? 0 : Math.trunc(Number(delta) || 0);
597
+ if (delta === 0) return;
598
+ var next = this._index + delta;
599
+ if (next < 0 || next > this._entries.length - 1) return;
600
+ this._index = next;
601
+ var ev = new Event('popstate');
602
+ ev.state = this._entries[this._index];
603
+ dispatchEvent(ev);
604
+ };
605
+ History.prototype.back = function() { this.go(-1); };
606
+ History.prototype.forward = function() { this.go(1); };
607
+
608
+ globalThis.History = History;
609
+ globalThis.history = new History();
610
+ })();
611
+ )JS";
612
+
613
+ // ── window.getSelection ──────────────────────────────────────────────────
614
+ // No layout engine backs this sandbox, so there is nothing to select — this
615
+ // mirrors matchMedia's stance: an always-empty Selection rather than a
616
+ // missing global, so defensive `window.getSelection().toString()` checks in
617
+ // third-party scripts don't crash the sandbox.
618
+
619
+ const char* kSelectionBootstrapScript = R"JS(
620
+ (function() {
621
+ function Selection() {}
622
+ Object.defineProperty(Selection.prototype, 'rangeCount', { get: function() { return 0; }, enumerable: true });
623
+ Object.defineProperty(Selection.prototype, 'isCollapsed', { get: function() { return true; }, enumerable: true });
624
+ Object.defineProperty(Selection.prototype, 'anchorNode', { get: function() { return null; }, enumerable: true });
625
+ Object.defineProperty(Selection.prototype, 'anchorOffset', { get: function() { return 0; }, enumerable: true });
626
+ Object.defineProperty(Selection.prototype, 'focusNode', { get: function() { return null; }, enumerable: true });
627
+ Object.defineProperty(Selection.prototype, 'focusOffset', { get: function() { return 0; }, enumerable: true });
628
+ Object.defineProperty(Selection.prototype, 'type', { get: function() { return 'None'; }, enumerable: true });
629
+ Selection.prototype.toString = function() { return ''; };
630
+ Selection.prototype.removeAllRanges = function() {};
631
+ Selection.prototype.collapse = function() {};
632
+ Selection.prototype.collapseToStart = function() {};
633
+ Selection.prototype.collapseToEnd = function() {};
634
+ Selection.prototype.selectAllChildren = function() {};
635
+ Selection.prototype.addRange = function() {};
636
+ Selection.prototype.containsNode = function() { return false; };
637
+ Selection.prototype.getRangeAt = function(index) {
638
+ throw new DOMException(
639
+ "Failed to execute 'getRangeAt' on 'Selection': " + index + ' is not a valid index.', 'IndexSizeError');
640
+ };
641
+ Selection.prototype.empty = Selection.prototype.removeAllRanges;
642
+
643
+ var selectionInstance = new Selection();
644
+ globalThis.Selection = Selection;
645
+ globalThis.getSelection = function() { return selectionInstance; };
646
+ })();
647
+ )JS";
648
+
396
649
  // ── matchMedia ─────────────────────────────────────────────────────────────
397
650
  // No layout engine backs this sandbox, so every query reports "no match" —
398
651
  // mirrors jsdom's own stance that layout-dependent APIs return inert defaults
@@ -435,6 +688,7 @@ void WindowBindings::install(JSContext* ctx) {
435
688
  JS_SetPropertyStr(ctx, global, "alert", JS_NewCFunction(ctx, js_window_alert, "alert", 1));
436
689
  JS_SetPropertyStr(ctx, global, "confirm", JS_NewCFunction(ctx, js_window_confirm, "confirm", 1));
437
690
  JS_SetPropertyStr(ctx, global, "prompt", JS_NewCFunction(ctx, js_window_prompt, "prompt", 2));
691
+ JS_SetPropertyStr(ctx, global, "reportError", JS_NewCFunction(ctx, js_window_reportError, "reportError", 1));
438
692
  JS_SetPropertyStr(ctx, global, "atob", JS_NewCFunction(ctx, js_window_atob, "atob", 1));
439
693
  JS_SetPropertyStr(ctx, global, "btoa", JS_NewCFunction(ctx, js_window_btoa, "btoa", 1));
440
694
 
@@ -456,6 +710,27 @@ void WindowBindings::install(JSContext* ctx) {
456
710
  }
457
711
  JS_FreeValue(ctx, location_result);
458
712
 
713
+ JSValue history_result = JS_Eval(ctx, kHistoryBootstrapScript, strlen(kHistoryBootstrapScript),
714
+ "<history-bootstrap>", JS_EVAL_TYPE_GLOBAL);
715
+ if (JS_IsException(history_result)) {
716
+ JS_FreeValue(ctx, JS_GetException(ctx));
717
+ }
718
+ JS_FreeValue(ctx, history_result);
719
+
720
+ JSValue selection_result = JS_Eval(ctx, kSelectionBootstrapScript, strlen(kSelectionBootstrapScript),
721
+ "<selection-bootstrap>", JS_EVAL_TYPE_GLOBAL);
722
+ if (JS_IsException(selection_result)) {
723
+ JS_FreeValue(ctx, JS_GetException(ctx));
724
+ }
725
+ JS_FreeValue(ctx, selection_result);
726
+
727
+ JSValue console_extras_result = JS_Eval(ctx, kConsoleExtrasBootstrapScript, strlen(kConsoleExtrasBootstrapScript),
728
+ "<console-extras-bootstrap>", JS_EVAL_TYPE_GLOBAL);
729
+ if (JS_IsException(console_extras_result)) {
730
+ JS_FreeValue(ctx, JS_GetException(ctx));
731
+ }
732
+ JS_FreeValue(ctx, console_extras_result);
733
+
459
734
  JSValue crypto_result = JS_Eval(ctx, kCryptoBootstrapScript, strlen(kCryptoBootstrapScript),
460
735
  "<crypto-bootstrap>", JS_EVAL_TYPE_GLOBAL);
461
736
  if (JS_IsException(crypto_result)) {
@@ -476,6 +751,13 @@ void WindowBindings::install(JSContext* ctx) {
476
751
  JS_FreeValue(ctx, JS_GetException(ctx));
477
752
  }
478
753
  JS_FreeValue(ctx, match_media_result);
754
+
755
+ JSValue structured_clone_result = JS_Eval(ctx, kStructuredCloneBootstrapScript, strlen(kStructuredCloneBootstrapScript),
756
+ "<structured-clone-bootstrap>", JS_EVAL_TYPE_GLOBAL);
757
+ if (JS_IsException(structured_clone_result)) {
758
+ JS_FreeValue(ctx, JS_GetException(ctx));
759
+ }
760
+ JS_FreeValue(ctx, structured_clone_result);
479
761
  }
480
762
 
481
763
  } // namespace margelo::nitro::nitrojsdom
@@ -1,8 +1,9 @@
1
1
  #pragma once
2
2
 
3
3
  // window.alert/confirm/prompt (bridged to onAlert/onConfirm/onPrompt native
4
- // callbacks), console.log/warn/error/info/debug, and the window.location
5
- // bootstrap script.
4
+ // callbacks), console.log/warn/error/info/debug, and the window.location,
5
+ // window.history, window.getSelection, crypto, navigator, matchMedia, and
6
+ // structuredClone bootstrap scripts.
6
7
 
7
8
  #include "quickjs.h"
8
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salve-software/react-native-nitro-jsdom",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "packageManager": "yarn@4.9.1",
5
5
  "description": "A headless HTML/DOM environment for React Native.",
6
6
  "main": "./lib/commonjs/index.js",
@@ -24,7 +24,22 @@
24
24
  },
25
25
  "keywords": [
26
26
  "react-native",
27
- "@salve-software/react-native-nitro-jsdom"
27
+ "react-native-jsdom",
28
+ "react-native-nitro-jsdom",
29
+ "nitro-modules",
30
+ "nitro",
31
+ "expo",
32
+ "jsdom",
33
+ "dom",
34
+ "html",
35
+ "html-parser",
36
+ "quickjs",
37
+ "lexbor",
38
+ "sandbox",
39
+ "headless-browser",
40
+ "webview-alternative",
41
+ "ios",
42
+ "android"
28
43
  ],
29
44
  "files": [
30
45
  "src",