@webjsdev/server 0.8.58 → 0.8.60
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.
- package/index.d.ts +125 -12
- package/index.js +2 -0
- package/package.json +3 -3
- package/src/action-seed-bun.js +2 -2
- package/src/action-seed.js +438 -70
- package/src/actions.js +10 -2
- package/src/base-path.js +4 -1
- package/src/body-limit.js +4 -4
- package/src/check.js +123 -2
- package/src/component-elision.js +75 -16
- package/src/component-scanner.js +82 -26
- package/src/context.js +1 -1
- package/src/csp.js +22 -6
- package/src/csrf.js +33 -12
- package/src/dev-error.js +13 -2
- package/src/dev-overlay.js +218 -6
- package/src/dev.js +151 -33
- package/src/elision-differential.js +101 -0
- package/src/elision-report.js +133 -30
- package/src/form-action-identity.js +290 -0
- package/src/form-dispatch.js +666 -0
- package/src/forwarded.js +68 -10
- package/src/headers.js +9 -7
- package/src/html-cache.js +117 -13
- package/src/js-scan.js +41 -13
- package/src/listener-bun.js +1 -1
- package/src/listener-core.js +35 -5
- package/src/rate-limit.js +31 -3
- package/src/route-types.js +1 -1
- package/src/ssr.js +93 -17
- package/src/stream.js +1 -1
- package/src/testing.d.ts +19 -0
- package/src/testing.js +113 -0
- package/src/vendor.js +85 -22
- package/webjs-config.schema.json +19 -3
- package/src/page-action.js +0 -259
package/src/csrf.js
CHANGED
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
* 2. When `Sec-Fetch-Site` is absent (an older browser), fall back to
|
|
20
20
|
* comparing the `Origin` host to the request host; an absent `Origin`
|
|
21
21
|
* can't be checked so it passes (a handcrafted / non-browser request
|
|
22
|
-
* can't carry a victim's SameSite cookies cross-site anyway).
|
|
22
|
+
* can't carry a victim's SameSite cookies cross-site anyway). That
|
|
23
|
+
* request host comes from `requestHost` below, which resolves
|
|
24
|
+
* `x-forwarded-host` through the shared `trustProxy()` seam in
|
|
25
|
+
* `forwarded.js` (#1104), so this file holds no trust posture of its own.
|
|
23
26
|
*
|
|
24
27
|
* Scope:
|
|
25
28
|
* - Internal RPC only. A `route.ts` REST endpoint (hand-written or via the
|
|
@@ -28,6 +31,8 @@
|
|
|
28
31
|
* - Session / auth cookies stay `SameSite=Lax` as defense-in-depth.
|
|
29
32
|
*/
|
|
30
33
|
|
|
34
|
+
import { trustProxy } from './forwarded.js';
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Parse cookies off a standard Request. Retained as a general cookie reader
|
|
33
38
|
* (used by `context.js` for `cookies()`), independent of CSRF.
|
|
@@ -56,21 +61,37 @@ function hostOf(value) {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
|
-
* The host the request was addressed to
|
|
60
|
-
*
|
|
61
|
-
*
|
|
64
|
+
* The host the request was addressed to: `x-forwarded-host` when a proxy is
|
|
65
|
+
* trusted to speak for the client (the Cloudflare-in-front-of-Railway setup),
|
|
66
|
+
* then the `Host` header, then the request URL.
|
|
67
|
+
*
|
|
68
|
+
* Trust note, preserved from before this read went through `trustProxy()`
|
|
69
|
+
* (#1104), because it is correct and non-obvious: reading `x-forwarded-host`
|
|
70
|
+
* here was never a CSRF weakness. A CSRF attack is browser-driven and a
|
|
71
|
+
* browser cannot set `x-forwarded-host` (or `Origin`); only a direct
|
|
72
|
+
* non-browser client can, and such a client carries no victim SameSite
|
|
73
|
+
* cookies to abuse. The primary `Sec-Fetch-Site` path does not reach this
|
|
74
|
+
* function at all, only the legacy no-`Sec-Fetch-Site` fallback in
|
|
75
|
+
* `verifyOrigin` does. The value CAN still be attacker-chosen, since a proxy
|
|
76
|
+
* of the kind above forwards a client-supplied one rather than overwriting
|
|
77
|
+
* it; see the threat model in `forwarded.js` for what that does reach.
|
|
78
|
+
*
|
|
79
|
+
* What changed is the POSTURE, not a hole: the read now honors
|
|
80
|
+
* `WEBJS_NO_TRUST_PROXY=1` like every other forwarded-header read, so an
|
|
81
|
+
* operator who sets the flag because their container is directly reachable
|
|
82
|
+
* gets one answer from the whole package instead of two. Behind a proxy
|
|
83
|
+
* WITHOUT the flag nothing changes. With the flag set on a genuinely proxied
|
|
84
|
+
* deploy (a misconfiguration) the fallback compares `Origin` against the raw
|
|
85
|
+
* `Host`, so a legitimate cross-host request can now be rejected. That is the
|
|
86
|
+
* flag's intended meaning.
|
|
62
87
|
*
|
|
63
|
-
* Trust note: `x-forwarded-host` is only meaningful behind a proxy that
|
|
64
|
-
* overwrites it. This is NOT a CSRF weakness, because a CSRF attack is
|
|
65
|
-
* browser-driven and a browser cannot set `x-forwarded-host` (or `Origin`);
|
|
66
|
-
* only a direct non-browser client can, and such a client carries no victim
|
|
67
|
-
* SameSite cookies to abuse. The primary `Sec-Fetch-Site` path does not use
|
|
68
|
-
* this at all.
|
|
69
88
|
* @param {Request} req
|
|
70
89
|
*/
|
|
71
90
|
export function requestHost(req) {
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
if (trustProxy()) {
|
|
92
|
+
const xfh = req.headers.get('x-forwarded-host');
|
|
93
|
+
if (xfh) return xfh.split(',')[0].trim().toLowerCase();
|
|
94
|
+
}
|
|
74
95
|
const host = req.headers.get('host');
|
|
75
96
|
if (host) return host.toLowerCase();
|
|
76
97
|
return hostOf(req.url);
|
package/src/dev-error.js
CHANGED
|
@@ -91,9 +91,19 @@ export function readCodeFrame(file, line, column, context = 3) {
|
|
|
91
91
|
/**
|
|
92
92
|
* Build a dev error frame from an error. DEV-ONLY (the caller gates on `dev`).
|
|
93
93
|
*
|
|
94
|
+
* `opts.url` stamps the frame with the request URL that produced it (#1047).
|
|
95
|
+
* Only a `render` frame carries one, and the browser overlay uses it to refuse a
|
|
96
|
+
* frame belonging to a page the tab is not looking at (another tab's crash, a
|
|
97
|
+
* background render of some other url, a page navigated away from). A frame with
|
|
98
|
+
* no url renders unconditionally, which is what a `rebuild` / `ts-strip` frame
|
|
99
|
+
* wants: those describe a still-broken build, not one URL.
|
|
100
|
+
*
|
|
101
|
+
* A speculative link prefetch is NOT in that list, and cannot be: `dev.js` drops
|
|
102
|
+
* the report hook for one, so no frame is ever built for it to refuse.
|
|
103
|
+
*
|
|
94
104
|
* @param {unknown} error
|
|
95
|
-
* @param {{ kind?: 'render' | 'ts-strip' | 'rebuild', appDir?: string, file?: string, line?: number, column?: number, hint?: string }} [opts]
|
|
96
|
-
* @returns {{ kind: string, message: string, stack: string|null, file: string|null, line: number|null, column: number|null, codeFrame: string|null, hint: string|null }}
|
|
105
|
+
* @param {{ kind?: 'render' | 'ts-strip' | 'rebuild', appDir?: string, file?: string, line?: number, column?: number, hint?: string, url?: string }} [opts]
|
|
106
|
+
* @returns {{ kind: string, message: string, stack: string|null, file: string|null, line: number|null, column: number|null, codeFrame: string|null, hint: string|null, url: string|null }}
|
|
97
107
|
*/
|
|
98
108
|
export function buildDevErrorFrame(error, opts = {}) {
|
|
99
109
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -125,5 +135,6 @@ export function buildDevErrorFrame(error, opts = {}) {
|
|
|
125
135
|
column,
|
|
126
136
|
codeFrame,
|
|
127
137
|
hint: opts.hint || null,
|
|
138
|
+
url: typeof opts.url === 'string' && opts.url ? opts.url : null,
|
|
128
139
|
};
|
|
129
140
|
}
|
package/src/dev-overlay.js
CHANGED
|
@@ -9,14 +9,83 @@
|
|
|
9
9
|
* Security: the overlay is built with `createElement` + `textContent` only,
|
|
10
10
|
* NEVER `innerHTML`, so a hostile error message / file path / code frame is
|
|
11
11
|
* rendered as inert text and can never inject markup or script.
|
|
12
|
+
*
|
|
13
|
+
* Scope (#1047): a `render` frame is stamped server-side with the URL that
|
|
14
|
+
* produced it, and this module is the single gate deciding whether a frame
|
|
15
|
+
* belongs on the page currently being viewed. Without it, a body-level overlay
|
|
16
|
+
* outlives every client-router swap (which works strictly inside the keyed
|
|
17
|
+
* boundary comment ranges), and any render of a page this tab is NOT looking at
|
|
18
|
+
* raises an overlay over the one it is, in every open tab, since the frame fans
|
|
19
|
+
* out over the shared SSE channel. (The other half of #1047, a link prefetch
|
|
20
|
+
* reporting a frame at all, is cut server-side in `dev.js`, not here.)
|
|
21
|
+
*
|
|
22
|
+
* The gate cannot be a plain refuse-and-drop, because the SSE frame is pushed
|
|
23
|
+
* DURING the render, before the navigation response is even sent, so it reaches
|
|
24
|
+
* the browser while `location` is still the old page. So there are two slots: a
|
|
25
|
+
* RENDERED frame and a PENDING one the gate refused. A refused frame is held,
|
|
26
|
+
* and the nav sync re-evaluates it once the URL advances.
|
|
27
|
+
*
|
|
28
|
+
* A held frame is only ever rendered for the navigation it actually belongs to,
|
|
29
|
+
* which is what `__wjNavSeq` is for. A frame that arrives while a navigation is
|
|
30
|
+
* IN FLIGHT is that navigation's; one that arrived while the tab sat idle is
|
|
31
|
+
* not, and must never paint on a later visit to that url, because by then the
|
|
32
|
+
* page may well render fine. An idle-time frame comes from a render this tab
|
|
33
|
+
* did not navigate for: another tab's page, or a background fetch of some other
|
|
34
|
+
* url. NOT from a link prefetch, which reports no frame at all. Both look
|
|
35
|
+
* identical once held, so the seq records which navigation was in flight when
|
|
36
|
+
* the frame landed, and the sync renders it only if that is still the one
|
|
37
|
+
* finishing. `webjs:before-cache` is the nav-START signal (the router snapshots
|
|
38
|
+
* the page it is leaving before it fetches); a navigation that does not emit one
|
|
39
|
+
* leaves the seq unchanged, which fails toward SHOWING the frame, the same way
|
|
40
|
+
* the rest of the gate fails open.
|
|
12
41
|
*/
|
|
13
42
|
|
|
14
43
|
/** The single live overlay element, or null. */
|
|
15
44
|
let __wjOverlay = null;
|
|
45
|
+
/** The frame the live overlay was built from, or null. */
|
|
46
|
+
let __wjFrame = null;
|
|
47
|
+
/** The most recent frame the scope gate refused, awaiting a URL that matches. */
|
|
48
|
+
let __wjPending = null;
|
|
49
|
+
/** The navigation that was in flight when `__wjPending` arrived. */
|
|
50
|
+
let __wjPendingSeq = -1;
|
|
51
|
+
/** Bumped at the start of each client-router navigation. */
|
|
52
|
+
let __wjNavSeq = 0;
|
|
16
53
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
54
|
+
/** The path the gate compares a frame's url against. */
|
|
55
|
+
function __wjCurrentPath() {
|
|
56
|
+
return typeof location !== 'undefined' ? location.pathname + location.search : '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Compare a frame url against a browser path. Encoding-tolerant on purpose: a
|
|
61
|
+
* mismatch fails CLOSED (no overlay), so a percent-encoding difference on a
|
|
62
|
+
* dynamic segment would otherwise hide a genuine error.
|
|
63
|
+
*/
|
|
64
|
+
function __wjSamePath(a, b) {
|
|
65
|
+
if (a === b) return true;
|
|
66
|
+
try { return decodeURI(a) === decodeURI(b); } catch (_) { return false; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True when `f` is a frame the scope gate applies to (a url-stamped render). */
|
|
70
|
+
function __wjScoped(f) {
|
|
71
|
+
return !!f && f.kind === 'render' && typeof f.url === 'string' && f.url !== '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Take the overlay off the page. Leaves the pending slot alone. */
|
|
75
|
+
function __wjRemoveOverlay() {
|
|
19
76
|
if (__wjOverlay) { __wjOverlay.remove(); __wjOverlay = null; }
|
|
77
|
+
__wjFrame = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Remove the overlay if one is showing, and forget any pending frame. This is
|
|
82
|
+
* what the Dismiss button calls, so a frame the user dismissed by hand is never
|
|
83
|
+
* resurrected by a later navigation.
|
|
84
|
+
*/
|
|
85
|
+
export function dismissDevOverlay() {
|
|
86
|
+
__wjRemoveOverlay();
|
|
87
|
+
__wjPending = null;
|
|
88
|
+
__wjPendingSeq = -1;
|
|
20
89
|
}
|
|
21
90
|
|
|
22
91
|
/** Append a styled text row to `parent`. */
|
|
@@ -30,11 +99,27 @@ function __wjRow(parent, css, text) {
|
|
|
30
99
|
|
|
31
100
|
/**
|
|
32
101
|
* Render the dev error overlay for a frame, replacing any prior one.
|
|
33
|
-
*
|
|
102
|
+
*
|
|
103
|
+
* A url-stamped `render` frame for a DIFFERENT path than `currentPath` is not
|
|
104
|
+
* rendered; it is held in the pending slot for `syncDevOverlayToLocation` to
|
|
105
|
+
* re-evaluate once the URL advances (#1047). The gate runs BEFORE any removal,
|
|
106
|
+
* so a refused frame never wipes what is on screen (a live `rebuild` /
|
|
107
|
+
* `ts-strip` overlay describes a still-broken build and must survive it) and
|
|
108
|
+
* never paints even briefly. A frame with no `url`, or any other kind, renders
|
|
109
|
+
* exactly as it always did.
|
|
110
|
+
*
|
|
111
|
+
* @param {{ kind?: string, message?: string, file?: string|null, line?: number|null, column?: number|null, codeFrame?: string|null, hint?: string|null, url?: string|null }} f
|
|
112
|
+
* @param {string} [currentPath] defaults to `location.pathname + location.search`
|
|
34
113
|
*/
|
|
35
|
-
export function renderDevOverlay(f) {
|
|
36
|
-
dismissDevOverlay();
|
|
37
|
-
|
|
114
|
+
export function renderDevOverlay(f, currentPath) {
|
|
115
|
+
if (!f) { dismissDevOverlay(); return; }
|
|
116
|
+
const here = currentPath === undefined ? __wjCurrentPath() : currentPath;
|
|
117
|
+
if (__wjScoped(f) && !__wjSamePath(f.url, here)) {
|
|
118
|
+
__wjPending = f;
|
|
119
|
+
__wjPendingSeq = __wjNavSeq;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
__wjRemoveOverlay();
|
|
38
123
|
const o = document.createElement('div');
|
|
39
124
|
o.setAttribute('data-webjs-error-overlay', '');
|
|
40
125
|
o.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:rgba(10,10,12,.92);color:#e6e6e6;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;padding:32px;overflow:auto';
|
|
@@ -72,4 +157,131 @@ export function renderDevOverlay(f) {
|
|
|
72
157
|
o.appendChild(card);
|
|
73
158
|
(document.body || document.documentElement).appendChild(o);
|
|
74
159
|
__wjOverlay = o;
|
|
160
|
+
__wjFrame = f;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Re-evaluate the scope gate against the page now being viewed (#1047). Called
|
|
165
|
+
* on every client-router navigation, so an overlay the navigation just made
|
|
166
|
+
* stale comes down, and a frame that arrived before the URL advanced (the usual
|
|
167
|
+
* ordering, since the SSE frame is pushed during the render that produces the
|
|
168
|
+
* navigation response) goes up.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} [currentPath] defaults to `location.pathname + location.search`
|
|
171
|
+
*/
|
|
172
|
+
export function syncDevOverlayToLocation(currentPath) {
|
|
173
|
+
const here = currentPath === undefined ? __wjCurrentPath() : currentPath;
|
|
174
|
+
// The BACKSTOP for an overlay element this module does not own. Any such node
|
|
175
|
+
// is unremovable and its Dismiss button carries no listener, so it would sit
|
|
176
|
+
// there forever: a `document.body.replaceChildren` from cached HTML is the way
|
|
177
|
+
// that happens. The router's OWN back/forward snapshot is already handled
|
|
178
|
+
// upstream, since `before-cache` detaches the overlay across its `outerHTML`
|
|
179
|
+
// read, so what is left here is everything else (a snapshot cached before this
|
|
180
|
+
// client installed its listeners, or any other wholesale body replacement).
|
|
181
|
+
// Also notice when the node we did own was replaced out from under us: it is
|
|
182
|
+
// detached, so `.remove()` would no-op and the slot would lie about what is
|
|
183
|
+
// on screen.
|
|
184
|
+
if (typeof document !== 'undefined') {
|
|
185
|
+
const live = __wjOverlay && __wjOverlay.isConnected ? __wjOverlay : null;
|
|
186
|
+
__wjOverlay = live;
|
|
187
|
+
document.querySelectorAll('[data-webjs-error-overlay]').forEach((el) => {
|
|
188
|
+
if (el !== live) el.remove();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// A live overlay whose page we have navigated away from comes down. An
|
|
192
|
+
// unscoped overlay (rebuild / ts-strip) is untouched: it describes the build,
|
|
193
|
+
// not one URL, and only the next successful rebuild clears it.
|
|
194
|
+
if (__wjScoped(__wjFrame) && !__wjSamePath(__wjFrame.url, here)) __wjRemoveOverlay();
|
|
195
|
+
// Still the right frame for this page, but its node was swept away by the
|
|
196
|
+
// restore above: put a REAL one back, with a working Dismiss button.
|
|
197
|
+
if (__wjFrame && !__wjOverlay) renderDevOverlay(__wjFrame, here);
|
|
198
|
+
// The pending frame is consumed by this navigation either way. It renders
|
|
199
|
+
// only if this is BOTH the page it names and the navigation it belongs to;
|
|
200
|
+
// a frame held from an idle-time render (another tab's page, a background
|
|
201
|
+
// fetch of some other url) is dropped, because by the time you actually
|
|
202
|
+
// visit that url the page may render perfectly well, and an overlay over it
|
|
203
|
+
// would be the very bug this whole gate exists to stop.
|
|
204
|
+
const p = __wjPending;
|
|
205
|
+
const seq = __wjPendingSeq;
|
|
206
|
+
__wjPending = null;
|
|
207
|
+
__wjPendingSeq = -1;
|
|
208
|
+
if (p && seq === __wjNavSeq && __wjSamePath(p.url, here)) renderDevOverlay(p, here);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Mark the start of a client-router navigation (#1047), so a frame arriving
|
|
213
|
+
* from here on is known to belong to it. Exported for the browser test; the
|
|
214
|
+
* shipping wiring is `installDevOverlayNavSync`.
|
|
215
|
+
*/
|
|
216
|
+
export function markDevOverlayNavStart() {
|
|
217
|
+
__wjNavSeq++;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Wire the scope gate to the client router's navigation events (#1047), so the
|
|
222
|
+
* overlay tracks the page actually on screen. Browser-safe and dev-only: the
|
|
223
|
+
* reload client that calls this is served only in dev.
|
|
224
|
+
*
|
|
225
|
+
* Three events, each for a distinct reason:
|
|
226
|
+
* - `webjs:navigate` on `document`, the applied-navigation signal, dispatched
|
|
227
|
+
* after `history.pushState` so `location` is already current.
|
|
228
|
+
* - `popstate` on `window`, because a back/forward restore served from the
|
|
229
|
+
* router's snapshot cache returns before it dispatches `webjs:navigate`.
|
|
230
|
+
* - `webjs:before-cache` on `document`, which does two things. It is the
|
|
231
|
+
* nav-START marker (the router dispatches it from `snapshotCurrent`, at the
|
|
232
|
+
* top of every navigation and form submission, before the fetch), the only
|
|
233
|
+
* signal separating a frame belonging to the navigation in flight from one
|
|
234
|
+
* held since the tab was idle. And it detaches the overlay across the
|
|
235
|
+
* snapshot read, re-attaching it a microtask later, so the cached HTML
|
|
236
|
+
* carries no copy. What it must NOT do is strip the overlay for good, which
|
|
237
|
+
* the event's own contract invites: it fires on EVERY navigation, so that
|
|
238
|
+
* would tear a `rebuild` / `ts-strip` overlay off the page the moment you
|
|
239
|
+
* clicked any link, while the build was still broken.
|
|
240
|
+
*
|
|
241
|
+
* @param {{ document?: any, window?: any, getPath?: () => string }} [opts] injection seam for the browser test
|
|
242
|
+
* @returns {() => void} an uninstall thunk
|
|
243
|
+
*/
|
|
244
|
+
export function installDevOverlayNavSync(opts) {
|
|
245
|
+
const o = opts || {};
|
|
246
|
+
const doc = o.document || (typeof document !== 'undefined' ? document : null);
|
|
247
|
+
const win = o.window || (typeof window !== 'undefined' ? window : null);
|
|
248
|
+
const getPath = o.getPath || __wjCurrentPath;
|
|
249
|
+
const onNav = () => syncDevOverlayToLocation(getPath());
|
|
250
|
+
const onBeforeCache = () => {
|
|
251
|
+
markDevOverlayNavStart();
|
|
252
|
+
// Keep the overlay out of the snapshot the router is about to take. It
|
|
253
|
+
// reads `outerHTML` SYNCHRONOUSLY right after this event, so detaching here
|
|
254
|
+
// and re-attaching in a microtask (which runs once that read has returned,
|
|
255
|
+
// and long before any paint) means the cached HTML never carries a copy,
|
|
256
|
+
// while the overlay on screen never flickers.
|
|
257
|
+
//
|
|
258
|
+
// This is not the strip that used to live here. That one dropped the frame
|
|
259
|
+
// permanently, so any link click tore down a `rebuild` / `ts-strip`
|
|
260
|
+
// overlay while the build was still broken; this puts the SAME node back.
|
|
261
|
+
// The sweep in `syncDevOverlayToLocation` stays as the backstop, because
|
|
262
|
+
// it cannot cover every ordering on its own: under an opt-in view
|
|
263
|
+
// transition the router defers the body swap past the sync, so a copy
|
|
264
|
+
// parsed in afterwards would otherwise sit there undismissable.
|
|
265
|
+
const el = __wjOverlay;
|
|
266
|
+
if (!el || !el.isConnected) return;
|
|
267
|
+
const parent = el.parentNode;
|
|
268
|
+
el.remove();
|
|
269
|
+
queueMicrotask(() => {
|
|
270
|
+
// Only if it is still the live overlay: a frame arriving meanwhile may
|
|
271
|
+
// have replaced it, or the user may have dismissed it.
|
|
272
|
+
if (__wjOverlay === el && !el.isConnected && parent) parent.appendChild(el);
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
if (doc) {
|
|
276
|
+
doc.addEventListener('webjs:navigate', onNav);
|
|
277
|
+
doc.addEventListener('webjs:before-cache', onBeforeCache);
|
|
278
|
+
}
|
|
279
|
+
if (win) win.addEventListener('popstate', onNav);
|
|
280
|
+
return () => {
|
|
281
|
+
if (doc) {
|
|
282
|
+
doc.removeEventListener('webjs:navigate', onNav);
|
|
283
|
+
doc.removeEventListener('webjs:before-cache', onBeforeCache);
|
|
284
|
+
}
|
|
285
|
+
if (win) win.removeEventListener('popstate', onNav);
|
|
286
|
+
};
|
|
75
287
|
}
|