hypha-debugger 0.1.5 → 0.1.6

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.
@@ -46,6 +46,7 @@ export declare class HyphaDebugger {
46
46
  private server;
47
47
  private serviceInfo;
48
48
  private boundBeforeUnload;
49
+ private cleanupInterceptor;
49
50
  constructor(config: DebuggerConfig);
50
51
  start(): Promise<DebugSession>;
51
52
  destroy(): Promise<void>;
@@ -10614,8 +10614,6 @@
10614
10614
  }
10615
10615
  /**
10616
10616
  * Inject the debugger loader script into HTML before </body> (or append).
10617
- * Also injects a tiny inline script that reads sessionStorage and passes
10618
- * config to the debugger via a global, so autoStart() picks it up.
10619
10617
  */
10620
10618
  function injectLoader(html, scriptUrl) {
10621
10619
  const loader = `<script src="${scriptUrl}"><\/script>`;
@@ -10629,8 +10627,8 @@
10629
10627
  }
10630
10628
  /**
10631
10629
  * Perform a soft page replacement: fetch HTML, inject debugger script,
10632
- * replace the document. Returns false if it can't be done (caller should
10633
- * fall back to hard navigation).
10630
+ * replace the document via document.write(). If the fetch or write fails,
10631
+ * falls back to hard navigation.
10634
10632
  */
10635
10633
  function softReplace(url, pushState) {
10636
10634
  const scriptUrl = getScriptUrl();
@@ -10668,6 +10666,133 @@
10668
10666
  }
10669
10667
  });
10670
10668
  }
10669
+ /** Check if a URL is same-origin as the current page. */
10670
+ function isSameOrigin(url) {
10671
+ try {
10672
+ const target = new URL(url, location.href);
10673
+ return target.origin === location.origin;
10674
+ }
10675
+ catch {
10676
+ return false;
10677
+ }
10678
+ }
10679
+ // ── Global navigation interception ────────────────────────────────────
10680
+ let _interceptInstalled = false;
10681
+ /**
10682
+ * Install global listeners that intercept same-origin link clicks and
10683
+ * form submissions, routing them through soft navigation so the debugger
10684
+ * stays connected.
10685
+ *
10686
+ * Called once from HyphaDebugger.start().
10687
+ */
10688
+ function installNavigationInterceptor() {
10689
+ if (_interceptInstalled)
10690
+ return () => { };
10691
+ _interceptInstalled = true;
10692
+ /**
10693
+ * Click handler: intercept <a> clicks that would navigate to a
10694
+ * same-origin HTML page.
10695
+ */
10696
+ const onClick = (e) => {
10697
+ // Skip if modifier keys (new tab, etc.) or not left-click
10698
+ if (e.defaultPrevented || e.button !== 0)
10699
+ return;
10700
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
10701
+ return;
10702
+ // Walk up from target to find the nearest <a>
10703
+ let anchor = null;
10704
+ let el = e.target;
10705
+ while (el) {
10706
+ if (el.tagName === "A") {
10707
+ anchor = el;
10708
+ break;
10709
+ }
10710
+ el = el.parentElement;
10711
+ }
10712
+ if (!anchor)
10713
+ return;
10714
+ const href = anchor.href;
10715
+ if (!href)
10716
+ return;
10717
+ // Skip non-http(s), download links, target=_blank, javascript:, #hash-only
10718
+ if (anchor.target && anchor.target !== "_self")
10719
+ return;
10720
+ if (anchor.hasAttribute("download"))
10721
+ return;
10722
+ if (href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:"))
10723
+ return;
10724
+ // Skip hash-only links (same page anchor)
10725
+ try {
10726
+ const target = new URL(href, location.href);
10727
+ if (target.origin === location.origin &&
10728
+ target.pathname === location.pathname &&
10729
+ target.search === location.search &&
10730
+ target.hash !== location.hash) {
10731
+ return; // Just a hash change, let browser handle it
10732
+ }
10733
+ }
10734
+ catch {
10735
+ return;
10736
+ }
10737
+ // Skip cross-origin
10738
+ if (!isSameOrigin(href))
10739
+ return;
10740
+ // Intercept: prevent default navigation and do soft replace
10741
+ e.preventDefault();
10742
+ const targetUrl = new URL(href, location.href).href;
10743
+ softReplace(targetUrl, targetUrl);
10744
+ };
10745
+ /**
10746
+ * Submit handler: intercept form submissions to same-origin action URLs.
10747
+ * Only handles GET forms (POST forms need the request body which is harder
10748
+ * to replicate via fetch).
10749
+ */
10750
+ const onSubmit = (e) => {
10751
+ if (e.defaultPrevented)
10752
+ return;
10753
+ const form = e.target;
10754
+ const method = (form.method || "GET").toUpperCase();
10755
+ // Only intercept GET forms — POST forms are too complex to replicate
10756
+ if (method !== "GET")
10757
+ return;
10758
+ const action = form.action || location.href;
10759
+ if (!isSameOrigin(action))
10760
+ return;
10761
+ // Build the URL with form data as query params
10762
+ const formData = new FormData(form);
10763
+ const url = new URL(action, location.href);
10764
+ for (const [key, value] of formData.entries()) {
10765
+ if (typeof value === "string") {
10766
+ url.searchParams.set(key, value);
10767
+ }
10768
+ }
10769
+ // Skip if target is _blank or similar
10770
+ if (form.target && form.target !== "_self")
10771
+ return;
10772
+ e.preventDefault();
10773
+ softReplace(url.href, url.href);
10774
+ };
10775
+ /**
10776
+ * Popstate handler: intercept browser back/forward (bfcache miss).
10777
+ * When the browser navigates via back/forward and there's no bfcache,
10778
+ * we can catch it via popstate and do a soft load of the target URL.
10779
+ */
10780
+ const onPopState = () => {
10781
+ // The URL has already changed when popstate fires.
10782
+ // Do a soft load of the current URL (which is the target of back/forward).
10783
+ softReplace(location.href);
10784
+ };
10785
+ document.addEventListener("click", onClick, true); // capture phase
10786
+ document.addEventListener("submit", onSubmit, true);
10787
+ window.addEventListener("popstate", onPopState);
10788
+ // Return cleanup function
10789
+ return () => {
10790
+ document.removeEventListener("click", onClick, true);
10791
+ document.removeEventListener("submit", onSubmit, true);
10792
+ window.removeEventListener("popstate", onPopState);
10793
+ _interceptInstalled = false;
10794
+ };
10795
+ }
10671
10796
  // ── navigate ──────────────────────────────────────────────────────────
10672
10797
  function navigate(url) {
10673
10798
  try {
@@ -10718,7 +10843,7 @@
10718
10843
  window.history.back();
10719
10844
  return {
10720
10845
  success: true,
10721
- message: "Navigated back (re-click bookmarklet to reconnect if needed)",
10846
+ message: "Navigated back (debugger will auto-reconnect via popstate)",
10722
10847
  };
10723
10848
  }
10724
10849
  catch (err) {
@@ -10730,7 +10855,7 @@
10730
10855
  }
10731
10856
  goBack.__schema__ = {
10732
10857
  name: "goBack",
10733
- description: "Navigate back in browser history. The debugger may disconnect — re-click the bookmarklet to reconnect.",
10858
+ description: "Navigate back in browser history. The debugger auto-reconnects for same-origin pages.",
10734
10859
  parameters: {
10735
10860
  type: "object",
10736
10861
  properties: {},
@@ -10741,7 +10866,7 @@
10741
10866
  window.history.forward();
10742
10867
  return {
10743
10868
  success: true,
10744
- message: "Navigated forward (re-click bookmarklet to reconnect if needed)",
10869
+ message: "Navigated forward (debugger will auto-reconnect via popstate)",
10745
10870
  };
10746
10871
  }
10747
10872
  catch (err) {
@@ -10753,7 +10878,7 @@
10753
10878
  }
10754
10879
  goForward.__schema__ = {
10755
10880
  name: "goForward",
10756
- description: "Navigate forward in browser history. The debugger may disconnect — re-click the bookmarklet to reconnect.",
10881
+ description: "Navigate forward in browser history. The debugger auto-reconnects for same-origin pages.",
10757
10882
  parameters: {
10758
10883
  type: "object",
10759
10884
  properties: {},
@@ -13895,6 +14020,7 @@
13895
14020
  this.server = null;
13896
14021
  this.serviceInfo = null;
13897
14022
  this.boundBeforeUnload = null;
14023
+ this.cleanupInterceptor = null;
13898
14024
  const requireToken = config.require_token ?? false;
13899
14025
  // Always append random suffix unless user provided a custom id.
13900
14026
  let serviceId = config.service_id ?? "web-debugger";
@@ -13958,6 +14084,9 @@
13958
14084
  this.saveConfigToStorage();
13959
14085
  this.boundBeforeUnload = () => this.saveConfigToStorage();
13960
14086
  window.addEventListener("beforeunload", this.boundBeforeUnload);
14087
+ // Intercept same-origin link clicks, form submits, and popstate
14088
+ // so the debugger survives user-initiated navigation
14089
+ this.cleanupInterceptor = installNavigationInterceptor();
13961
14090
  return session;
13962
14091
  }
13963
14092
  catch (err) {
@@ -13973,11 +14102,15 @@
13973
14102
  }
13974
14103
  }
13975
14104
  async destroy() {
13976
- // Remove beforeunload listener
14105
+ // Remove event listeners
13977
14106
  if (this.boundBeforeUnload) {
13978
14107
  window.removeEventListener("beforeunload", this.boundBeforeUnload);
13979
14108
  this.boundBeforeUnload = null;
13980
14109
  }
14110
+ if (this.cleanupInterceptor) {
14111
+ this.cleanupInterceptor();
14112
+ this.cleanupInterceptor = null;
14113
+ }
13981
14114
  try {
13982
14115
  if (this.serviceInfo && this.server) {
13983
14116
  await this.server.unregisterService(this.serviceInfo.id);
@@ -14386,6 +14519,7 @@
14386
14519
  exports.goForward = goForward;
14387
14520
  exports.inputText = inputText;
14388
14521
  exports.installConsoleCapture = installConsoleCapture;
14522
+ exports.installNavigationInterceptor = installNavigationInterceptor;
14389
14523
  exports.navigate = navigate;
14390
14524
  exports.queryDom = queryDom;
14391
14525
  exports.reload = reload;
@@ -14393,6 +14527,7 @@
14393
14527
  exports.scroll = scroll;
14394
14528
  exports.scrollTo = scrollTo;
14395
14529
  exports.selectOption = selectOption;
14530
+ exports.softReplace = softReplace;
14396
14531
  exports.startDebugger = startDebugger;
14397
14532
  exports.takeScreenshot = takeScreenshot;
14398
14533
  exports.wrapFn = wrapFn;