capybara-simulated 0.8.0 → 0.10.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 +43 -18
- data/lib/capybara/simulated/asset_cache.rb +25 -12
- data/lib/capybara/simulated/browser.rb +4182 -338
- data/lib/capybara/simulated/driver.rb +205 -30
- data/lib/capybara/simulated/errors.rb +12 -0
- data/lib/capybara/simulated/js/bridge.bundle.js +14151 -3473
- data/lib/capybara/simulated/minitest.rb +22 -0
- data/lib/capybara/simulated/node.rb +18 -13
- data/lib/capybara/simulated/quickjs_runtime.rb +55 -10
- data/lib/capybara/simulated/runtime_shared.rb +71 -33
- data/lib/capybara/simulated/stack_resolver.rb +5 -0
- data/lib/capybara/simulated/trace.rb +38 -11
- data/lib/capybara/simulated/trace_persistence.rb +30 -4
- data/lib/capybara/simulated/trace_viewer.html +561 -207
- data/lib/capybara/simulated/v8_runtime.rb +265 -70
- data/lib/capybara/simulated/version.rb +1 -1
- data/lib/capybara/simulated/worker_runtime.rb +34 -11
- data/lib/capybara/simulated.rb +14 -0
- data/vendor/js/vendor.bundle.js +14 -14
- metadata +15 -1
|
@@ -11,8 +11,17 @@
|
|
|
11
11
|
|
|
12
12
|
require 'digest'
|
|
13
13
|
require 'fileutils'
|
|
14
|
+
require 'uri'
|
|
14
15
|
require 'rusty_racer'
|
|
15
16
|
|
|
17
|
+
# The engine is a SOFT dependency (the gemspec names no version), so say what
|
|
18
|
+
# we need here rather than letting it surface as a NoMethodError from inside
|
|
19
|
+
# `rebuild_ctx`'s warm-reset rescue, which would report it as a failed reset.
|
|
20
|
+
# 0.2.1: Context#eval_void / Script#run_void. 0.2.0: Module#graph_async?.
|
|
21
|
+
unless RustyRacer::Context.method_defined?(:eval_void)
|
|
22
|
+
raise LoadError, "capybara-simulated needs rusty_racer >= 0.2.1 (found #{RustyRacer::VERSION})"
|
|
23
|
+
end
|
|
24
|
+
|
|
16
25
|
require_relative 'runtime_shared'
|
|
17
26
|
require_relative 'script_cache'
|
|
18
27
|
require_relative 'worker_runtime'
|
|
@@ -27,7 +36,7 @@ begin
|
|
|
27
36
|
# 8900×8900 fixture through the transfer-buffer path). Match
|
|
28
37
|
# Discourse's own testem flag of 4 GB so the test fits.
|
|
29
38
|
#
|
|
30
|
-
# rusty_racer
|
|
39
|
+
# rusty_racer installs a near-heap-limit callback on every isolate,
|
|
31
40
|
# so EXCEEDING this cap raises a catchable `RustyRacer::V8OutOfMemoryError`
|
|
32
41
|
# (and the isolate recovers) instead of V8 aborting the whole process with a
|
|
33
42
|
# fatal "Reached heap limit". So this value doubles as the memory backstop: a
|
|
@@ -113,6 +122,7 @@ module Capybara
|
|
|
113
122
|
# policy), so a returned eval/call has already run its end-of-script
|
|
114
123
|
# microtasks.
|
|
115
124
|
def eval(src) = @ctx.eval(src)
|
|
125
|
+
def eval_void(src) = @ctx.eval_void(src)
|
|
116
126
|
def call(name, *args) = @ctx.call(name, *args)
|
|
117
127
|
|
|
118
128
|
# Record every attach so `create_context` can replay them: the bridge
|
|
@@ -152,8 +162,8 @@ module Capybara
|
|
|
152
162
|
def terminate = @iso.terminate
|
|
153
163
|
def dispose = @iso.dispose
|
|
154
164
|
def perform_microtask_checkpoint = @iso.perform_microtask_checkpoint
|
|
155
|
-
# V8 heap accounting + a forced full GC
|
|
156
|
-
#
|
|
165
|
+
# V8 heap accounting + a forced full GC. Used by the per-visit
|
|
166
|
+
# heap-pressure relief in `rebuild_ctx`.
|
|
157
167
|
def heap_statistics = @iso.heap_statistics
|
|
158
168
|
def low_memory_notification = @iso.low_memory_notification
|
|
159
169
|
|
|
@@ -336,6 +346,7 @@ module Capybara
|
|
|
336
346
|
end
|
|
337
347
|
|
|
338
348
|
def eval(code) = ctx.eval(code.to_s)
|
|
349
|
+
def eval_void(code) = ctx.eval_void(code.to_s)
|
|
339
350
|
def call(name, *args)
|
|
340
351
|
result = ctx.call(name, *args)
|
|
341
352
|
ScriptCache.warm_pending!
|
|
@@ -426,8 +437,15 @@ module Capybara
|
|
|
426
437
|
# through its own navigation.
|
|
427
438
|
def window_realm_meta = (@window_realm_meta ||= {})
|
|
428
439
|
|
|
440
|
+
# Is this realm a TOP-LEVEL browsing context? The main realm is, and so is an auxiliary
|
|
441
|
+
# window realm — `frame_realm_parents` records 0 for one because it has no parent FRAME,
|
|
442
|
+
# which is not the same as being nested in the main window. An ancestor walk (the focus
|
|
443
|
+
# chain, most of all) has to stop here rather than step into the opener.
|
|
444
|
+
def top_level_realm?(realm_id) = realm_id.to_i.zero? || window_realm_meta.key?(realm_id.to_i)
|
|
445
|
+
|
|
429
446
|
def dispose_frame_realms
|
|
430
447
|
@realm_module_handles&.clear
|
|
448
|
+
@module_sw_ctxs&.clear
|
|
431
449
|
@frame_realm_depths&.clear
|
|
432
450
|
@frame_realm_parents&.clear
|
|
433
451
|
@window_realm_meta&.clear
|
|
@@ -443,14 +461,14 @@ module Capybara
|
|
|
443
461
|
# / module state, exactly like the main page's per-visit rebuild. `parent_id`
|
|
444
462
|
# keeps the new realm's `parent`/`top` wired to the owning realm. The
|
|
445
463
|
# Browser then re-points the iframe element at the new id (`__csimRebindFrameRealm`).
|
|
446
|
-
def reload_frame_realm(old_id, parent_id, url, body, content_type)
|
|
464
|
+
def reload_frame_realm(old_id, parent_id, url, body, content_type, client_id = nil)
|
|
447
465
|
# A re-navigated document discards its child browsing contexts, so dispose the old realm's
|
|
448
466
|
# DESCENDANT frame realms too — not just old_id. The JS src-reassignment path gets this for
|
|
449
467
|
# free (the old document's iframe elements go away → DOM-unregister disposes their realms);
|
|
450
468
|
# the Ruby reload path (navigate_realm_self_get/_post) rebuilds without that DOM teardown, so
|
|
451
469
|
# a descendant frame's realm would otherwise linger and its contentWindow stay live.
|
|
452
470
|
dispose_frame_realm_tree(old_id)
|
|
453
|
-
create_frame_realm(ctx, url, body, content_type, parent_id)
|
|
471
|
+
create_frame_realm(ctx, url, body, content_type, parent_id, nil, nil, nil, nil, nil, nil, client_id)
|
|
454
472
|
end
|
|
455
473
|
|
|
456
474
|
# Dispose a frame realm and every descendant frame realm (transitively), deepest first so a
|
|
@@ -471,7 +489,11 @@ module Capybara
|
|
|
471
489
|
# Like reload_frame_realm, a re-navigated window discards its child browsing contexts — dispose
|
|
472
490
|
# the descendant frame realms too, not just old_id, so a popup's iframes don't linger.
|
|
473
491
|
dispose_frame_realm_tree(old_id)
|
|
474
|
-
create_window_realm(
|
|
492
|
+
create_window_realm(
|
|
493
|
+
url, body, content_type,
|
|
494
|
+
opener_id: meta[:opener_id], window_name: meta[:window_name],
|
|
495
|
+
about_base: meta[:about_base], about_origin: meta[:about_origin]
|
|
496
|
+
)
|
|
475
497
|
end
|
|
476
498
|
|
|
477
499
|
# Tear down a single frame realm (e.g. a descendant frame destroyed when an
|
|
@@ -483,7 +505,13 @@ module Capybara
|
|
|
483
505
|
@browser.revoke_realm_blobs(id) rescue nil
|
|
484
506
|
# Drop it from the SW Client registry so matchAll stops returning a dead client.
|
|
485
507
|
@browser.sw_unregister_client(id) rescue nil
|
|
508
|
+
# A dedicated worker is owned by the context that created it — discarding the context
|
|
509
|
+
# terminates it (and unregisters its own client record).
|
|
510
|
+
@browser.terminate_realm_workers(id) rescue nil
|
|
511
|
+
# If it held the focus chain, focus returns to the top-level browsing context.
|
|
512
|
+
@browser.note_realm_discarded(id) rescue nil
|
|
486
513
|
@realm_module_handles&.delete(id)
|
|
514
|
+
@module_sw_ctxs&.delete(id)
|
|
487
515
|
@frame_realm_depths&.delete(id)
|
|
488
516
|
@frame_realm_parents&.delete(id)
|
|
489
517
|
@window_realm_meta&.delete(id)
|
|
@@ -492,7 +520,7 @@ module Capybara
|
|
|
492
520
|
# unregister path (its iframe element going away), but a WINDOW realm has no
|
|
493
521
|
# element — this is its only eviction, and it covers the reload (dispose +
|
|
494
522
|
# recreate) path too, where the old id would otherwise leak in the set.
|
|
495
|
-
ctx.
|
|
523
|
+
ctx.eval_void("globalThis.__csimChildRealmIds && globalThis.__csimChildRealmIds.delete(#{id.to_i});") rescue nil
|
|
496
524
|
fr = frame_realms.delete(id)
|
|
497
525
|
fr.dispose rescue nil if fr
|
|
498
526
|
nil
|
|
@@ -583,7 +611,7 @@ module Capybara
|
|
|
583
611
|
# contexts N -> 1). See `relieve_heap_pressure`.
|
|
584
612
|
relieve_heap_pressure
|
|
585
613
|
attach_host_fns(@ctx)
|
|
586
|
-
@ctx.
|
|
614
|
+
@ctx.eval_void('__csim_installWorker();')
|
|
587
615
|
return @ctx
|
|
588
616
|
rescue StandardError => e
|
|
589
617
|
warn "[capybara-simulated] warm context reset failed, falling back to cold rebuild: #{e.class}: #{e.message}"
|
|
@@ -711,7 +739,7 @@ module Capybara
|
|
|
711
739
|
# occasional JS-side infinite loop would otherwise stall the whole
|
|
712
740
|
# run; the timeout converts the hang into a
|
|
713
741
|
# `RustyRacer::ScriptTerminatedError` on that one example — whose
|
|
714
|
-
# `#message` / `#js_backtrace`
|
|
742
|
+
# `#message` / `#js_backtrace` name the looping JS
|
|
715
743
|
# frame (function + source position), so an in-V8 hang is diagnosable
|
|
716
744
|
# from the failure alone, no live debugger attach needed. The
|
|
717
745
|
# terminate escalates through any nested frames (it is
|
|
@@ -733,7 +761,7 @@ module Capybara
|
|
|
733
761
|
def build_ctx
|
|
734
762
|
c = Ctx.new(snapshot: @snapshot || self.class.snapshot, timeout: CALL_TIMEOUT_MS)
|
|
735
763
|
attach_host_fns(c)
|
|
736
|
-
c.
|
|
764
|
+
c.eval_void('__csim_installWorker();')
|
|
737
765
|
c
|
|
738
766
|
end
|
|
739
767
|
|
|
@@ -753,8 +781,8 @@ module Capybara
|
|
|
753
781
|
# id (or nil on failure — then the bridge keeps its same-realm fallback).
|
|
754
782
|
# The bridge maps `iframe.contentWindow` to `RustyRacer.contextGlobal(id)`.
|
|
755
783
|
def attach_frame_realm_loader(c)
|
|
756
|
-
c.attach('__csim_createFrameRealm', ->(url, body, content_type, parent_id = 0, frame_name = nil, frame_doc_origin = nil, frame_location_origin = nil, js_url_source = nil, frame_about_base = nil) {
|
|
757
|
-
RuntimeShared.safe_call { create_frame_realm(c, url, body, content_type, parent_id, frame_name, frame_doc_origin, frame_location_origin, js_url_source, frame_about_base) }
|
|
784
|
+
c.attach('__csim_createFrameRealm', ->(url, body, content_type, parent_id = 0, frame_name = nil, frame_doc_origin = nil, frame_location_origin = nil, js_url_source = nil, frame_about_base = nil, frame_viewport = nil, client_id = nil) {
|
|
785
|
+
RuntimeShared.safe_call { create_frame_realm(c, url, body, content_type, parent_id, frame_name, frame_doc_origin, frame_location_origin, js_url_source, frame_about_base, frame_viewport, client_id) }
|
|
758
786
|
})
|
|
759
787
|
# Re-navigating an iframe (src/srcdoc reassigned) builds a fresh realm;
|
|
760
788
|
# the bridge calls this to tear down the superseded one so it doesn't
|
|
@@ -799,14 +827,14 @@ module Capybara
|
|
|
799
827
|
# document — rebind realm-executing variants on top, then reseed per-realm JS.
|
|
800
828
|
def seed_realm_bridge(realm)
|
|
801
829
|
has_bridge = realm.eval("typeof __csimLoadDocument === 'function'")
|
|
802
|
-
realm.
|
|
830
|
+
realm.eval_void(RuntimeShared.snapshot_src) unless has_bridge
|
|
803
831
|
attach_run_script_with_cache(realm)
|
|
804
832
|
attach_realm_esm_entry(realm)
|
|
805
833
|
reseed_realm_js(realm)
|
|
806
834
|
realm
|
|
807
835
|
end
|
|
808
836
|
|
|
809
|
-
def create_frame_realm(parent_ctx, url, body, content_type, parent_id = 0, frame_name = nil, frame_doc_origin = nil, frame_location_origin = nil, js_url_source = nil, frame_about_base = nil)
|
|
837
|
+
def create_frame_realm(parent_ctx, url, body, content_type, parent_id = 0, frame_name = nil, frame_doc_origin = nil, frame_location_origin = nil, js_url_source = nil, frame_about_base = nil, frame_viewport = nil, client_id = nil)
|
|
810
838
|
depth = (frame_realm_depths[parent_id] || 0) + 1
|
|
811
839
|
if depth > MAX_FRAME_DEPTH
|
|
812
840
|
@browser.log_console('warn', "iframe nesting depth #{depth} exceeds #{MAX_FRAME_DEPTH}; not building #{url}")
|
|
@@ -826,7 +854,15 @@ module Capybara
|
|
|
826
854
|
# `top` propagates up the chain (the main realm's `top` is itself). A
|
|
827
855
|
# nested frame thus reaches its TRUE parent, not unconditionally the
|
|
828
856
|
# main frame. `parent_id` is an integer the marshaller carries verbatim.
|
|
829
|
-
|
|
857
|
+
#
|
|
858
|
+
# eval_void, and not by preference: a statement list's completion value is
|
|
859
|
+
# its last statement's, so this block evaluates to the WindowProxy it just
|
|
860
|
+
# assigned — and marshalling a value RUNS JS, so the proxy's `ownKeys` trap
|
|
861
|
+
# throws SecurityError at a cross-origin parent. That lands after every
|
|
862
|
+
# write here has already succeeded, aborting the rest of the frame's boot
|
|
863
|
+
# (leaving it on the snapshot's default origin) over a value we never asked
|
|
864
|
+
# for. Every eval below whose value we discard is spelled the same way.
|
|
865
|
+
realm.eval_void(<<~JS)
|
|
830
866
|
if (globalThis.#{HOST_NAMESPACE_NAME} && typeof globalThis.#{HOST_NAMESPACE_NAME}.contextGlobal === 'function') {
|
|
831
867
|
var __parentWin = globalThis.#{HOST_NAMESPACE_NAME}.contextGlobal(#{parent_id.to_i});
|
|
832
868
|
if (__parentWin) {
|
|
@@ -854,10 +890,24 @@ module Capybara
|
|
|
854
890
|
# HTML / control bytes survive (Ruby's String#inspect is NOT a faithful
|
|
855
891
|
# JS string escaper — it mangles \a, \e, and binary bytes).
|
|
856
892
|
realm.call('__csimUpdateLocation', url.to_s) unless url.to_s.empty?
|
|
893
|
+
# The navigation's reserved client id — this document's realm ADOPTS it as its
|
|
894
|
+
# service-worker Client identity (`event.resultingClientId` resolves to THIS
|
|
895
|
+
# client after commit). Seeded BEFORE the document loads so the realm's own
|
|
896
|
+
# client reports (sw-client.js clientId()) already carry it; the browser-side
|
|
897
|
+
# alias keeps message routing and record minting coherent (sw_adopt_client_id).
|
|
898
|
+
unless client_id.to_s.empty?
|
|
899
|
+
realm.eval_void("globalThis.__csimClientId = #{JSON.generate(client_id.to_s)};")
|
|
900
|
+
@browser.sw_adopt_client_id(realm.id, client_id.to_s)
|
|
901
|
+
end
|
|
857
902
|
# Set window.name from the container's `name` attribute BEFORE the document
|
|
858
903
|
# loads, so a frame whose load handler reads window.name to identify itself
|
|
859
904
|
# (declarative-shadow declarative-child-frame) sees it.
|
|
860
905
|
realm.call('__csimSetWindowName', frame_name.to_s) unless frame_name.nil?
|
|
906
|
+
# The frame's viewport is its container's content box, measured by the parent realm. Seed it
|
|
907
|
+
# BEFORE the document loads so load-time `innerWidth` reads and `@media` evaluation see the
|
|
908
|
+
# frame's own size rather than the top window's. `nil` = an unrendered container, which is a
|
|
909
|
+
# 0x0 window — pass it through rather than skipping, or the frame keeps the top-level size.
|
|
910
|
+
realm.call('__csimFrameViewportChanged', frame_viewport)
|
|
861
911
|
# Seed the frame's document origin (opaque/inherited) BEFORE the document
|
|
862
912
|
# loads, so its load-time scripts read the right self.origin. nil → a
|
|
863
913
|
# real-URL frame whose origin is its own location origin.
|
|
@@ -881,21 +931,31 @@ module Capybara
|
|
|
881
931
|
# (sw_note_realm_controller); recording it BEFORE the load also lets a NESTED frame built during
|
|
882
932
|
# this frame's load inherit the controller.
|
|
883
933
|
opaque = opaque_frame_url?(url)
|
|
884
|
-
# A sandboxed frame WITHOUT allow-same-origin has an OPAQUE origin (doc origin 'null')
|
|
885
|
-
# cross-origin to
|
|
886
|
-
# "sandboxed srcdoc should not inherit")
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
@browser.
|
|
896
|
-
@browser.
|
|
934
|
+
# A sandboxed frame WITHOUT allow-same-origin has an OPAQUE origin (doc origin 'null'), which
|
|
935
|
+
# is cross-origin to everything — so it is NOT CONTROLLED AT ALL. It inherits no controller
|
|
936
|
+
# from its creator (srcdoc-iframe: "sandboxed srcdoc should not inherit"), its subresource
|
|
937
|
+
# fetches never reach the SW, and it is absent from `clients.matchAll()`. Excluding it from
|
|
938
|
+
# only the client registry would not hold: a controlled frame's first subresource fetch
|
|
939
|
+
# re-registers it lazily from the fetch event (js/src/workers.js clientFor). The SW does still
|
|
940
|
+
# answer its NAVIGATION request, because a CSP-header sandbox is knowable only FROM that
|
|
941
|
+
# response (sandboxed-iframe-fetch-event, "Service worker should NOT control the sandboxed
|
|
942
|
+
# page"). allow-same-origin restores the parent origin, and a real-URL frame carries no
|
|
943
|
+
# explicit origin at all (nil = its own location origin) — both are controlled normally.
|
|
944
|
+
if frame_doc_origin.to_s != 'null'
|
|
945
|
+
ctrl = @browser.sw_client_controller_for(url.to_s)
|
|
946
|
+
ctrl ||= @browser.sw_inherited_controller_for(parent_id) if opaque
|
|
947
|
+
if ctrl
|
|
948
|
+
# Wiring the controller is what registers this frame as a client — the realm reports
|
|
949
|
+
# itself from installController (js/src/sw-client.js), before the document below loads.
|
|
950
|
+
realm.call('__csim_swSetControllerDirect', *ctrl)
|
|
951
|
+
@browser.sw_note_realm_controller(realm.id, ctrl)
|
|
952
|
+
end
|
|
897
953
|
end
|
|
898
954
|
realm.call('__csimLoadDocument', body.to_s, content_type.to_s)
|
|
955
|
+
# This document now has a URL and any host-wired controller, so it can announce itself
|
|
956
|
+
# as a service-worker client — an UNCONTROLLED context is still a client of its origin
|
|
957
|
+
# and must show up in `matchAll({includeUncontrolled: true})`.
|
|
958
|
+
realm.call('__csim_swReportClient') rescue nil
|
|
899
959
|
# A `javascript:` URL frame: the initial empty document is now loaded and
|
|
900
960
|
# parent/top are wired, so evaluate the URL's script in the realm (global
|
|
901
961
|
# scope). Per HTML, only a STRING result navigates the frame to a new
|
|
@@ -929,6 +989,7 @@ module Capybara
|
|
|
929
989
|
# including any module handles its scripts compiled before the throw.
|
|
930
990
|
if realm
|
|
931
991
|
@realm_module_handles&.delete(realm.id)
|
|
992
|
+
@module_sw_ctxs&.delete(realm.id)
|
|
932
993
|
realm.dispose rescue nil
|
|
933
994
|
end
|
|
934
995
|
nil
|
|
@@ -944,7 +1005,7 @@ module Capybara
|
|
|
944
1005
|
# a native `__csimFrameWindowProxyFor`, so `popup.document` is a real
|
|
945
1006
|
# same-isolate Document (cross-window adoptNode works). The single isolate also
|
|
946
1007
|
# makes a same-origin window far cheaper than today's isolate-per-window.
|
|
947
|
-
def create_window_realm(url, body, content_type, opener_id: nil, window_name: nil, doc_origin: nil, location_origin: nil)
|
|
1008
|
+
def create_window_realm(url, body, content_type, opener_id: nil, window_name: nil, doc_origin: nil, location_origin: nil, about_base: nil, about_origin: nil)
|
|
948
1009
|
realm = seed_realm_bridge(ctx.create_context)
|
|
949
1010
|
# Mark it a top-level window realm so its location setter routes to
|
|
950
1011
|
# __csimWindowRealmNavigate (reload THIS realm) rather than the frame-nav or
|
|
@@ -953,7 +1014,7 @@ module Capybara
|
|
|
953
1014
|
# `window.closed` (flag-backed) and `window.close()` (marks closed; the realm
|
|
954
1015
|
# lingers inert until the Browser tears the isolate down — matching a real
|
|
955
1016
|
# closed window whose proxy stays valid and reports closed === true).
|
|
956
|
-
realm.
|
|
1017
|
+
realm.eval_void(<<~JS)
|
|
957
1018
|
globalThis.__csimIsWindowRealm = true;
|
|
958
1019
|
globalThis.__csimWindowClosedFlag = false;
|
|
959
1020
|
try {
|
|
@@ -969,14 +1030,28 @@ module Capybara
|
|
|
969
1030
|
# guard is on nil, not on 0 (0 is falsy but real here). Assigning globalThis.opener
|
|
970
1031
|
# routes through the bridge's opener setter (stores the override the getter returns).
|
|
971
1032
|
unless opener_id.nil?
|
|
972
|
-
realm.
|
|
1033
|
+
realm.eval_void(<<~JS)
|
|
973
1034
|
if (typeof globalThis.__csimFrameWindowProxyFor === 'function') {
|
|
974
1035
|
var __op = globalThis.__csimFrameWindowProxyFor(#{opener_id.to_i});
|
|
975
1036
|
if (__op) globalThis.opener = __op;
|
|
976
1037
|
}
|
|
977
1038
|
JS
|
|
978
1039
|
end
|
|
979
|
-
|
|
1040
|
+
# `window.open()` with no URL opens about:blank — that IS the new document's URL, and a
|
|
1041
|
+
# realm built from the snapshot would otherwise keep reporting the OPENER's (making the
|
|
1042
|
+
# popup a phantom duplicate of its opener everywhere a document URL is read, the service-
|
|
1043
|
+
# worker client set included). Its ORIGIN and its BASE url are inherited from the opener,
|
|
1044
|
+
# exactly as an empty <iframe>'s are (create_frame_realm's frame_about_base): the origin
|
|
1045
|
+
# so the popup isn't cross-origin to the window that opened it, the base so relative
|
|
1046
|
+
# resolution inside it still targets the opener's document.
|
|
1047
|
+
if url.to_s.empty?
|
|
1048
|
+
realm.call('__csimUpdateLocation', 'about:blank')
|
|
1049
|
+
realm.call('__csimSetAboutBaseURL', about_base.to_s) unless about_base.to_s.empty?
|
|
1050
|
+
doc_origin ||= about_origin unless about_origin.to_s.empty?
|
|
1051
|
+
location_origin ||= about_origin unless about_origin.to_s.empty?
|
|
1052
|
+
else
|
|
1053
|
+
realm.call('__csimUpdateLocation', url.to_s)
|
|
1054
|
+
end
|
|
980
1055
|
realm.call('__csimSetWindowName', window_name.to_s) unless window_name.nil?
|
|
981
1056
|
realm.call('__csimSetDocumentOrigin', doc_origin.to_s) unless doc_origin.nil?
|
|
982
1057
|
realm.call('__csimSetLocationOrigin', location_origin.to_s) unless location_origin.nil?
|
|
@@ -991,12 +1066,14 @@ module Capybara
|
|
|
991
1066
|
# Register with the opener (main) realm's child-realm set so `drainChildRealms`
|
|
992
1067
|
# steps THIS realm's event loop too — otherwise its queued tasks (e.g. a
|
|
993
1068
|
# BroadcastChannel delivery from a blob document) never fire.
|
|
994
|
-
ctx.
|
|
1069
|
+
ctx.eval_void("(globalThis.__csimChildRealmIds || (globalThis.__csimChildRealmIds = new Set())).add(#{realm.id});")
|
|
995
1070
|
# Remember the window's opener / name so a self-navigation (reload_window_realm
|
|
996
1071
|
# builds a FRESH realm) can carry them across — a real popup keeps window.opener
|
|
997
1072
|
# and window.name through its own navigation.
|
|
998
|
-
window_realm_meta[realm.id] = {opener_id: opener_id, window_name: window_name}
|
|
1073
|
+
window_realm_meta[realm.id] = {opener_id: opener_id, window_name: window_name, about_base: about_base, about_origin: about_origin}
|
|
999
1074
|
realm.call('__csimLoadDocument', body.to_s, content_type.to_s)
|
|
1075
|
+
# As in create_frame_realm: an auxiliary window is a client of its origin too.
|
|
1076
|
+
realm.call('__csim_swReportClient') rescue nil
|
|
1000
1077
|
realm.call('__csimFireWindowLoad') rescue nil
|
|
1001
1078
|
realm.id
|
|
1002
1079
|
rescue StandardError => e
|
|
@@ -1019,9 +1096,19 @@ module Capybara
|
|
|
1019
1096
|
# by default, or a frame realm + its realm-local cache (Module handles are
|
|
1020
1097
|
# context-bound; sharing the main cache would link a frame's imports
|
|
1021
1098
|
# against main-context modules).
|
|
1022
|
-
def eval_esm_module(url, inline_src = nil, target: nil, handles: nil)
|
|
1099
|
+
def eval_esm_module(url, inline_src = nil, target: nil, handles: nil, sw: nil)
|
|
1023
1100
|
target ||= ctx
|
|
1024
1101
|
handles ||= native_module_handles
|
|
1102
|
+
# A CONTROLLED document's module-graph source fetches dispatch SW fetch
|
|
1103
|
+
# events (destination 'script', mode 'cors'). The context is recorded per
|
|
1104
|
+
# REALM (not scoped to this call) so a dynamic `import()` resolved later
|
|
1105
|
+
# by the isolate resolver still finds it; `root` carries the entry URL,
|
|
1106
|
+
# whose fetch alone uses the element's own `integrity` attribute.
|
|
1107
|
+
# KNOWN LOSS (documented divergence): one slot per realm means the LAST
|
|
1108
|
+
# evaluated script's fetch options win — a deferred import() from an
|
|
1109
|
+
# earlier script picks up the later script's credentials/root. Spec wants
|
|
1110
|
+
# per-referring-script options; no vendored test observes the difference.
|
|
1111
|
+
module_sw_ctxs[realm_key(target)] = sw&.merge(root: url.to_s)
|
|
1025
1112
|
m = native_module_for(url, inline_src, target, handles)
|
|
1026
1113
|
return nil unless m
|
|
1027
1114
|
begin
|
|
@@ -1062,7 +1149,27 @@ module Capybara
|
|
|
1062
1149
|
def native_module_for(url, inline_src, target, handles)
|
|
1063
1150
|
return handles[url] if handles.key?(url)
|
|
1064
1151
|
url_s = url.to_s
|
|
1065
|
-
src
|
|
1152
|
+
src = inline_src
|
|
1153
|
+
if src.nil? && (sw = module_sw_ctxs[realm_key(target)])
|
|
1154
|
+
# Only the graph's ENTRY fetch carries the element's integrity
|
|
1155
|
+
# attribute; every other module resolves integrity from the import map
|
|
1156
|
+
# (HTML "resolve a module integrity metadata").
|
|
1157
|
+
integ = url_s == sw[:root] ? sw[:integrity].to_s : ''
|
|
1158
|
+
integ = @browser.importmap_integrity(url_s) if integ.empty?
|
|
1159
|
+
r = @browser.sw_script_subresource_fetch(
|
|
1160
|
+
sw[:handle],
|
|
1161
|
+
url_s,
|
|
1162
|
+
sw[:client_id],
|
|
1163
|
+
sw[:referrer],
|
|
1164
|
+
'script',
|
|
1165
|
+
'cors',
|
|
1166
|
+
sw[:credentials] || 'same-origin',
|
|
1167
|
+
integrity: integ
|
|
1168
|
+
)
|
|
1169
|
+
return handles[url] = nil if r && r['blocked'] # respondWith failed the load
|
|
1170
|
+
src = r && r['body']
|
|
1171
|
+
end
|
|
1172
|
+
src ||= @browser.rack_fetch_body(url_s)
|
|
1066
1173
|
return handles[url] = nil unless src
|
|
1067
1174
|
body = module_body(url_s, src)
|
|
1068
1175
|
# No-cd warm path: once this isolate has compiled a URL, its in-memory
|
|
@@ -1118,8 +1225,8 @@ module Capybara
|
|
|
1118
1225
|
# realm-correctness as static `<script type=module>` via
|
|
1119
1226
|
# `attach_realm_esm_entry`.
|
|
1120
1227
|
def attach_native_module_loader(c)
|
|
1121
|
-
c.attach('__csim_evalEsmEntry', ->(url, inline) {
|
|
1122
|
-
RuntimeShared.safe_call { eval_esm_module(url, inline) }
|
|
1228
|
+
c.attach('__csim_evalEsmEntry', ->(url, inline, *sw) {
|
|
1229
|
+
RuntimeShared.safe_call { eval_esm_module(url, inline, sw: esm_sw_ctx(sw)) }
|
|
1123
1230
|
nil
|
|
1124
1231
|
})
|
|
1125
1232
|
c.dynamic_import_resolver = ->(specifier, referrer, initiating) {
|
|
@@ -1145,11 +1252,43 @@ module Capybara
|
|
|
1145
1252
|
(@realm_module_handles ||= {})[realm_id] ||= {}
|
|
1146
1253
|
end
|
|
1147
1254
|
|
|
1255
|
+
# realm -> SW fetch context for module-source fetches (see eval_esm_module).
|
|
1256
|
+
# Invalidated exactly like native_module_handles (a service worker OUTLIVES
|
|
1257
|
+
# navigation, so a stale entry would be ACTIVE, not inert: the next page's
|
|
1258
|
+
# dynamic import would dispatch fetch events for an uncontrolled document);
|
|
1259
|
+
# frame-realm entries also drop with their realm in the dispose paths.
|
|
1260
|
+
def module_sw_ctxs
|
|
1261
|
+
@module_sw_ctxs ||= {}
|
|
1262
|
+
key = [ctx.object_id, ctx.generation]
|
|
1263
|
+
if @module_sw_ctxs_key != key
|
|
1264
|
+
@module_sw_ctxs = {}
|
|
1265
|
+
@module_sw_ctxs_key = key
|
|
1266
|
+
end
|
|
1267
|
+
@module_sw_ctxs
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
def realm_key(target)
|
|
1271
|
+
target.equal?(ctx) ? 0 : target.id
|
|
1272
|
+
end
|
|
1273
|
+
|
|
1274
|
+
# The optional [handle, clientId, referrer, credentials, integrity] tail
|
|
1275
|
+
# moduleSwCtx (bridge.entry.js) appends to __csim_evalEsmEntry, or nil.
|
|
1276
|
+
def esm_sw_ctx(sw)
|
|
1277
|
+
return nil unless sw && sw[0].to_i.positive?
|
|
1278
|
+
{
|
|
1279
|
+
handle: sw[0].to_i,
|
|
1280
|
+
client_id: sw[1].to_s,
|
|
1281
|
+
referrer: sw[2].to_s,
|
|
1282
|
+
credentials: sw[3].to_s,
|
|
1283
|
+
integrity: sw[4].to_s
|
|
1284
|
+
}
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1148
1287
|
# Frame-document `<script type=module>` entry, bound to the realm.
|
|
1149
1288
|
def attach_realm_esm_entry(realm)
|
|
1150
|
-
realm.attach('__csim_evalEsmEntry', ->(url, inline) {
|
|
1289
|
+
realm.attach('__csim_evalEsmEntry', ->(url, inline, *sw) {
|
|
1151
1290
|
RuntimeShared.safe_call {
|
|
1152
|
-
eval_esm_module(url, inline, target: realm, handles: realm_module_handles(realm.id))
|
|
1291
|
+
eval_esm_module(url, inline, target: realm, handles: realm_module_handles(realm.id), sw: esm_sw_ctx(sw))
|
|
1153
1292
|
}
|
|
1154
1293
|
nil
|
|
1155
1294
|
})
|
|
@@ -1193,15 +1332,7 @@ module Capybara
|
|
|
1193
1332
|
# pay the rendezvous round-trip.
|
|
1194
1333
|
c.attach('__csim_runScriptCached', ->(label, body) {
|
|
1195
1334
|
RuntimeShared.safe_call {
|
|
1196
|
-
|
|
1197
|
-
# `script.run`'s return crosses the V8→Ruby boundary on the trivial
|
|
1198
|
-
# marshalling fast path. Without it, a large inline script ending
|
|
1199
|
-
# in a jQuery-ish expression returns a `ce.fn.init` (array-like,
|
|
1200
|
-
# non-cloneable) that drags through the deep-copy filter —
|
|
1201
|
-
# pure waste, since the value is discarded (`nil` below). The SHA keys
|
|
1202
|
-
# the bytecode cache on the COMPILED source, so the suffix must be
|
|
1203
|
-
# hashed and fed to `queue_warm` too (else cached_data is rejected).
|
|
1204
|
-
src = "#{body}\n;undefined"
|
|
1335
|
+
src = body.to_s
|
|
1205
1336
|
# No-cd warm path, mirroring `native_module_for`: once this
|
|
1206
1337
|
# isolate has compiled a (label, bytesize), re-visits compile
|
|
1207
1338
|
# straight against V8's source-keyed in-memory cache — skipping
|
|
@@ -1227,7 +1358,13 @@ module Capybara
|
|
|
1227
1358
|
@compiled_script_keys[key] = true if key
|
|
1228
1359
|
end
|
|
1229
1360
|
begin
|
|
1230
|
-
script
|
|
1361
|
+
# run_void, not run: a `<script>`'s completion value is nobody's
|
|
1362
|
+
# answer, and marshalling it would drag a large inline script's
|
|
1363
|
+
# trailing jQuery-ish expression (a non-cloneable `ce.fn.init`)
|
|
1364
|
+
# through the deep-copy filter for nothing. This is why `src` is
|
|
1365
|
+
# the body verbatim — it used to carry a `;undefined` suffix, which
|
|
1366
|
+
# then had to be hashed into the bytecode-cache key as well.
|
|
1367
|
+
script.run_void
|
|
1231
1368
|
ensure
|
|
1232
1369
|
script.dispose
|
|
1233
1370
|
end
|
|
@@ -1269,17 +1406,15 @@ module Capybara
|
|
|
1269
1406
|
# as the QuickJS runner does. Swallowing here would turn a
|
|
1270
1407
|
# throwing leading-`const` inline script into a silent `load`.
|
|
1271
1408
|
c.attach('__csim_runScriptEval', ->(label, body) {
|
|
1272
|
-
#
|
|
1273
|
-
#
|
|
1274
|
-
#
|
|
1275
|
-
#
|
|
1276
|
-
#
|
|
1277
|
-
#
|
|
1278
|
-
#
|
|
1279
|
-
# doesn't affect the completion value; lexical declarations persist as a
|
|
1409
|
+
# A `<script>`'s completion value is nobody's answer, and reading it is
|
|
1410
|
+
# not free: a leading-lexical inline script ending in a jQuery-ish
|
|
1411
|
+
# expression (`const cfg=…; $(…)`) evaluates to a `ce.fn.init`
|
|
1412
|
+
# (array-like, non-cloneable) that drags through the deep-copy filter,
|
|
1413
|
+
# and a hostile getter in there would raise an error this call has no
|
|
1414
|
+
# business raising. `eval_void` says so outright, replacing the trailing
|
|
1415
|
+
# `;undefined` this used to append. Lexical declarations persist as a
|
|
1280
1416
|
# side effect of eval, independent of the completion value.
|
|
1281
|
-
c.
|
|
1282
|
-
nil
|
|
1417
|
+
c.eval_void("#{body}\n//# sourceURL=#{label.to_s.tr("\n", ' ')}")
|
|
1283
1418
|
})
|
|
1284
1419
|
install_run_script_dispatcher(c)
|
|
1285
1420
|
end
|
|
@@ -1291,7 +1426,7 @@ module Capybara
|
|
|
1291
1426
|
# run after the attaches it captures (`attach_run_script_with_cache`
|
|
1292
1427
|
# installs it last for exactly that reason).
|
|
1293
1428
|
def install_run_script_dispatcher(c)
|
|
1294
|
-
c.
|
|
1429
|
+
c.eval_void(<<~JS)
|
|
1295
1430
|
(function () {
|
|
1296
1431
|
const cached = globalThis.__csim_runScriptCached;
|
|
1297
1432
|
const runEval = globalThis.__csim_runScriptEval;
|
|
@@ -1299,6 +1434,18 @@ module Capybara
|
|
|
1299
1434
|
// Leading top-level lexical declaration, after optional BOM /
|
|
1300
1435
|
// whitespace / line+block comments / a "use strict" prologue.
|
|
1301
1436
|
const LEADS_LEXICAL = /^[\\s\\uFEFF]*(?:(?:\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/)\\s*)*(?:["']use strict["'];?\\s*)?(?:export\\s+)?(?:const|let|class)[\\s{\\[]/;
|
|
1437
|
+
// A top-level lexical declaration can also sit MID-file (WPT's
|
|
1438
|
+
// cookie-helper.sub.js opens with an IIFE and declares `const
|
|
1439
|
+
// wait_for_message` at line 129) — under `(0, eval)` it block-scopes
|
|
1440
|
+
// and later <script>s see a ReferenceError. Detect it by a
|
|
1441
|
+
// declaration at COLUMN 0 of any line: real top-level declarations
|
|
1442
|
+
// in unminified files start unindented, function-body ones are
|
|
1443
|
+
// indented. Residual known gap: a one-line `stmt; const CFG = …`
|
|
1444
|
+
// (declaration mid-LINE) still takes the fast path and loses the
|
|
1445
|
+
// binding — no observed page does this. A false positive (a template
|
|
1446
|
+
// literal's line starting with `const `) merely routes through the
|
|
1447
|
+
// always-correct ctx.eval path.
|
|
1448
|
+
const MID_LEXICAL = /^(?:const|let|class)[\\s{\\[]/m;
|
|
1302
1449
|
// A "use strict" directive prologue. A classic <script> evaluates as a
|
|
1303
1450
|
// top-level Script, where top-level `var` / `function` declarations
|
|
1304
1451
|
// bind on the global object even in strict mode — but the JS-only
|
|
@@ -1310,7 +1457,7 @@ module Capybara
|
|
|
1310
1457
|
const LEADS_USE_STRICT = /^[\\s\\uFEFF]*(?:(?:\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/)\\s*)*["']use strict["']/;
|
|
1311
1458
|
globalThis.__csim_runScript = function (label, body) {
|
|
1312
1459
|
if (body.length >= threshold) return cached(label, body);
|
|
1313
|
-
if (LEADS_LEXICAL.test(body) || LEADS_USE_STRICT.test(body)) return runEval(label || 'csim-eval', body);
|
|
1460
|
+
if (LEADS_LEXICAL.test(body) || LEADS_USE_STRICT.test(body) || MID_LEXICAL.test(body)) return runEval(label || 'csim-eval', body);
|
|
1314
1461
|
(0, eval)(body + '\\n//# sourceURL=' + (label || 'csim-eval'));
|
|
1315
1462
|
};
|
|
1316
1463
|
})();
|
|
@@ -1323,8 +1470,8 @@ module Capybara
|
|
|
1323
1470
|
# `__csim_installWorker()` post-snapshot init; the `__csim_runScript`
|
|
1324
1471
|
# dispatcher comes from `attach_run_script_with_cache` (realm-bound).
|
|
1325
1472
|
def reseed_realm_js(c)
|
|
1326
|
-
c.
|
|
1327
|
-
c.
|
|
1473
|
+
c.eval_void("globalThis.__csim_yield = globalThis.#{HOST_NAMESPACE_NAME}.drainMicrotasks;")
|
|
1474
|
+
c.eval_void('__csim_installWorker();')
|
|
1328
1475
|
end
|
|
1329
1476
|
|
|
1330
1477
|
# Class-level attach so Worker isolates (Ruby-thread-owned
|
|
@@ -1345,7 +1492,7 @@ module Capybara
|
|
|
1345
1492
|
# microtask-checkpoint semantics. Alias it to the namespace's native
|
|
1346
1493
|
# in-isolate checkpoint so callers pay ~sub-µs instead of an
|
|
1347
1494
|
# attached-fn cross-thread round-trip.
|
|
1348
|
-
c.
|
|
1495
|
+
c.eval_void("globalThis.__csim_yield = globalThis.#{HOST_NAMESPACE_NAME}.drainMicrotasks;")
|
|
1349
1496
|
# Register the bridge's recorder for V8's promise-reject notifications
|
|
1350
1497
|
# — the channel that surfaces rejections NO handler ever sees
|
|
1351
1498
|
# (fire-and-forget async functions, bare `Promise.reject`); the
|
|
@@ -1354,7 +1501,7 @@ module Capybara
|
|
|
1354
1501
|
# unhandled-rejection.js leaves registration to us. Main realm only —
|
|
1355
1502
|
# the recorder routes per-realm via `contextGlobal` itself, and a
|
|
1356
1503
|
# frame-realm registration would dangle once that realm is disposed.
|
|
1357
|
-
c.
|
|
1504
|
+
c.eval_void(<<~JS)
|
|
1358
1505
|
if (typeof globalThis.#{HOST_NAMESPACE_NAME}.setPromiseRejectHandler === 'function' &&
|
|
1359
1506
|
typeof globalThis.__csimPromiseRejected === 'function') {
|
|
1360
1507
|
globalThis.#{HOST_NAMESPACE_NAME}.setPromiseRejectHandler(globalThis.__csimPromiseRejected);
|
|
@@ -1375,7 +1522,14 @@ module Capybara
|
|
|
1375
1522
|
# deliver_worker_messages): client.postMessage, clients.claim (set the client's controller),
|
|
1376
1523
|
# and a controlled fetch's respondWith result. See run_worker for the closures.
|
|
1377
1524
|
c.attach('__csim_swPostToClient', ->(client_id, data) { sw_hooks[:post_to_client]&.call(client_id, data); nil }) if sw_hooks[:post_to_client]
|
|
1525
|
+
c.attach('__csim_swFocusClient', ->(client_id) { sw_hooks[:focus_client]&.call(client_id); nil }) if sw_hooks[:focus_client]
|
|
1526
|
+
c.attach('__csim_swNavigateClient', ->(client_id, url, nav_id) { sw_hooks[:navigate_client]&.call(client_id, url, nav_id); nil }) if sw_hooks[:navigate_client]
|
|
1378
1527
|
c.attach('__csim_swClaim', -> { sw_hooks[:claim]&.call; nil }) if sw_hooks[:claim]
|
|
1528
|
+
c.attach('__csim_swSkipWaitingRequest', -> { sw_hooks[:skip_waiting]&.call; nil }) if sw_hooks[:skip_waiting]
|
|
1529
|
+
c.attach('__csim_swNoteRouterRules', -> { sw_hooks[:router]&.call; nil }) if sw_hooks[:router]
|
|
1530
|
+
c.attach('__csim_swRaceNetwork', ->(fetch_id, realm_id, url, method) { sw_hooks[:race_network]&.call(fetch_id, realm_id, url, method); nil }) if sw_hooks[:race_network]
|
|
1531
|
+
c.attach('__csim_swUnregisterRequest', -> { sw_hooks[:unregister]&.call; nil }) if sw_hooks[:unregister]
|
|
1532
|
+
c.attach('__csim_swExtendedChanged', ->(n) { sw_hooks[:extended]&.call(n); nil }) if sw_hooks[:extended]
|
|
1379
1533
|
c.attach('__csim_swFetchRespond', ->(fetch_id, resp, realm_id) { sw_hooks[:fetch_respond]&.call(fetch_id, resp, realm_id); nil }) if sw_hooks[:fetch_respond]
|
|
1380
1534
|
c.attach('__csim_swFetchStream', ->(fetch_id, kind, payload, realm_id) { sw_hooks[:fetch_stream]&.call(fetch_id, kind, payload, realm_id); nil }) if sw_hooks[:fetch_stream]
|
|
1381
1535
|
# Cross-isolate MessagePort channel signals (a worker/SW port endpoint + its outbound messages).
|
|
@@ -1398,15 +1552,56 @@ module Capybara
|
|
|
1398
1552
|
# join the realm's shared global lexical environment where later code sees them.
|
|
1399
1553
|
# `(0, eval)` would block-scope them to the eval and they'd vanish. `c.eval` is
|
|
1400
1554
|
# the top-level-script path (same as the worker's own body eval).
|
|
1401
|
-
c.attach('__csim_workerImportEval', ->(src) { c.
|
|
1402
|
-
c.
|
|
1555
|
+
c.attach('__csim_workerImportEval', ->(src) { c.eval_void(src.to_s) })
|
|
1556
|
+
c.eval_void('__csim_installWorkerScope();')
|
|
1403
1557
|
WorkerRuntime.new(
|
|
1404
|
-
|
|
1558
|
+
eval_void_fn: ->(s) { c.eval_void(s.to_s) },
|
|
1405
1559
|
call_fn: ->(n, *a) { c.call(n.to_s, *a) },
|
|
1406
1560
|
drain_microtasks: -> { c.perform_microtask_checkpoint },
|
|
1407
1561
|
drain_timers: -> { c.call('__drainTimers', 50) },
|
|
1408
1562
|
has_ready_timer: -> { !!c.call('__hasReadyTimer') },
|
|
1409
|
-
dispose: -> { c.dispose rescue nil }
|
|
1563
|
+
dispose: -> { c.dispose rescue nil },
|
|
1564
|
+
# Called from the SESSION BOUNDARY's thread, not this worker's: V8's terminate is
|
|
1565
|
+
# thread-safe by design, and it is the only way to end a call that is already running.
|
|
1566
|
+
terminate: -> { c.terminate rescue nil },
|
|
1567
|
+
# A `{type: 'module'}` service worker's main script + static import graph,
|
|
1568
|
+
# via V8's native module API (the same surface the main realm's
|
|
1569
|
+
# eval_esm_module uses). The whole graph resolves through the root's
|
|
1570
|
+
# instantiate callback (V8 calls it per unresolved edge, transitively);
|
|
1571
|
+
# `fetch_import` runs on the worker's own thread and raises to fail the
|
|
1572
|
+
# evaluation. Specifier resolution is PLAIN URL resolution — a worker has
|
|
1573
|
+
# no document, so the page's importmap does not apply, and a bare
|
|
1574
|
+
# specifier is a resolution failure per the spec.
|
|
1575
|
+
eval_module_graph: lambda {|src, url, fetch_import|
|
|
1576
|
+
src_text = RuntimeShared.utf8_text(src.to_s.dup)
|
|
1577
|
+
handles = {}
|
|
1578
|
+
root = c.compile_module(src_text, filename: url.to_s)
|
|
1579
|
+
handles[url.to_s] = root
|
|
1580
|
+
root.instantiate do |spec, ref|
|
|
1581
|
+
s = spec.to_s
|
|
1582
|
+
resolved =
|
|
1583
|
+
if s.match?(%r{\A[a-z]+://}i)
|
|
1584
|
+
s
|
|
1585
|
+
elsif s.start_with?('/', './', '../')
|
|
1586
|
+
URI.join((ref || url).to_s, s).to_s
|
|
1587
|
+
else
|
|
1588
|
+
raise "Failed to resolve module specifier '#{s}'"
|
|
1589
|
+
end
|
|
1590
|
+
handles[resolved] ||= c.compile_module(RuntimeShared.utf8_text(fetch_import.call(resolved).to_s.dup), filename: resolved)
|
|
1591
|
+
end
|
|
1592
|
+
# Top-level await is disallowed in a service worker module ("Run Service
|
|
1593
|
+
# Worker" fails the script; Chrome rejects the registration). V8's
|
|
1594
|
+
# IsGraphAsync (Module#graph_async?) answers it for the WHOLE
|
|
1595
|
+
# instantiated graph, which is what the spec asks: TLA hiding in an
|
|
1596
|
+
# imported module fails too. Per-module `[[HasTLA]]` would name the
|
|
1597
|
+
# offender but not this question — it can't see an imported module's
|
|
1598
|
+
# await. This lambda serves service workers only; a dedicated module
|
|
1599
|
+
# worker, where TLA is legal, would need the check parameterized.
|
|
1600
|
+
raise 'Top-level await is disallowed in a service worker' if root.graph_async?
|
|
1601
|
+
|
|
1602
|
+
root.evaluate
|
|
1603
|
+
nil
|
|
1604
|
+
}
|
|
1410
1605
|
)
|
|
1411
1606
|
end
|
|
1412
1607
|
end
|