@modularizer/plat 0.8.0 → 0.9.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.
- package/README.md +75 -0
- package/dist/client-server-entry.d.ts +4 -0
- package/dist/client-server-entry.d.ts.map +1 -1
- package/dist/client-server-entry.js +4 -0
- package/dist/client-server-entry.js.map +1 -1
- package/dist/client-side-python/runtime.d.ts +1 -0
- package/dist/client-side-python/runtime.d.ts.map +1 -1
- package/dist/client-side-python/runtime.js +10 -1
- package/dist/client-side-python/runtime.js.map +1 -1
- package/dist/client-side-server/channel.d.ts +1 -1
- package/dist/client-side-server/channel.d.ts.map +1 -1
- package/dist/client-side-server/channel.js +113 -4
- package/dist/client-side-server/channel.js.map +1 -1
- package/dist/client-side-server/mqtt-webrtc.d.ts.map +1 -1
- package/dist/client-side-server/mqtt-webrtc.js +4 -0
- package/dist/client-side-server/mqtt-webrtc.js.map +1 -1
- package/dist/client-side-server/plat-bridge.d.ts +40 -0
- package/dist/client-side-server/plat-bridge.d.ts.map +1 -0
- package/dist/client-side-server/plat-bridge.js +876 -0
- package/dist/client-side-server/plat-bridge.js.map +1 -0
- package/dist/client-side-server/plat-fetch.d.ts +33 -0
- package/dist/client-side-server/plat-fetch.d.ts.map +1 -0
- package/dist/client-side-server/plat-fetch.js +199 -0
- package/dist/client-side-server/plat-fetch.js.map +1 -0
- package/dist/client-side-server/plat-service-worker-client.d.ts +30 -0
- package/dist/client-side-server/plat-service-worker-client.d.ts.map +1 -0
- package/dist/client-side-server/plat-service-worker-client.js +141 -0
- package/dist/client-side-server/plat-service-worker-client.js.map +1 -0
- package/dist/client-side-server/plat-service-worker.d.ts +35 -0
- package/dist/client-side-server/plat-service-worker.d.ts.map +1 -0
- package/dist/client-side-server/plat-service-worker.js +292 -0
- package/dist/client-side-server/plat-service-worker.js.map +1 -0
- package/dist/client-side-server/protocol.d.ts +22 -0
- package/dist/client-side-server/protocol.d.ts.map +1 -1
- package/dist/client-side-server/protocol.js.map +1 -1
- package/dist/client-side-server/runtime.d.ts +9 -0
- package/dist/client-side-server/runtime.d.ts.map +1 -1
- package/dist/client-side-server/runtime.js +37 -13
- package/dist/client-side-server/runtime.js.map +1 -1
- package/dist/client-side-server/server.d.ts +17 -0
- package/dist/client-side-server/server.d.ts.map +1 -1
- package/dist/client-side-server/server.js +111 -7
- package/dist/client-side-server/server.js.map +1 -1
- package/dist/generated/python-browser-sources.js +4 -4
- package/dist/generated/python-browser-sources.js.map +1 -1
- package/dist/server/core.d.ts +8 -0
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +21 -0
- package/dist/server/core.js.map +1 -1
- package/dist/server/server.d.ts +4 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +81 -2
- package/dist/server/server.js.map +1 -1
- package/dist/static/file-response.d.ts +42 -0
- package/dist/static/file-response.d.ts.map +1 -0
- package/dist/static/file-response.js +57 -0
- package/dist/static/file-response.js.map +1 -0
- package/dist/static/glob-match.d.ts +16 -0
- package/dist/static/glob-match.d.ts.map +1 -0
- package/dist/static/glob-match.js +48 -0
- package/dist/static/glob-match.js.map +1 -0
- package/dist/static/index.d.ts +9 -0
- package/dist/static/index.d.ts.map +1 -0
- package/dist/static/index.js +6 -0
- package/dist/static/index.js.map +1 -0
- package/dist/static/mime-types.d.ts +6 -0
- package/dist/static/mime-types.d.ts.map +1 -0
- package/dist/static/mime-types.js +74 -0
- package/dist/static/mime-types.js.map +1 -0
- package/dist/static/static-folder.d.ts +69 -0
- package/dist/static/static-folder.d.ts.map +1 -0
- package/dist/static/static-folder.js +206 -0
- package/dist/static/static-folder.js.map +1 -0
- package/dist/static/virtual-file-system.d.ts +45 -0
- package/dist/static/virtual-file-system.d.ts.map +1 -0
- package/dist/static/virtual-file-system.js +100 -0
- package/dist/static/virtual-file-system.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plat-bridge: Universal HTTP → CSS:// bridge
|
|
3
|
+
*
|
|
4
|
+
* Transparently intercepts all browser network requests (fetch, XHR, resource elements)
|
|
5
|
+
* and routes them through a plat client-side server channel instead of HTTP.
|
|
6
|
+
*
|
|
7
|
+
* Two modes:
|
|
8
|
+
*
|
|
9
|
+
* 1. Full-page (direct channel):
|
|
10
|
+
* import { installBridge } from '@modularizer/plat-client/client-server'
|
|
11
|
+
* installBridge(channel)
|
|
12
|
+
* // All fetch/XHR/resource loads now route through the channel
|
|
13
|
+
*
|
|
14
|
+
* 2. Iframe (postMessage):
|
|
15
|
+
* Parent injects generateBridgeScript() into iframe srcdoc, then handles
|
|
16
|
+
* 'plat-fetch' postMessages and responds with 'plat-fetch-response'.
|
|
17
|
+
*/
|
|
18
|
+
import { createPlatFetch } from './plat-fetch.js';
|
|
19
|
+
/**
|
|
20
|
+
* Install the plat bridge on the current page.
|
|
21
|
+
* All relative and css:// URLs will be routed through the given channel.
|
|
22
|
+
* Returns a handle to restore the original behavior.
|
|
23
|
+
*/
|
|
24
|
+
export function installBridge(channel) {
|
|
25
|
+
const platFetch = createPlatFetch({ channel });
|
|
26
|
+
return installBridgeWithFetch(platFetch);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Install the bridge using a custom fetch function.
|
|
30
|
+
* Useful if you already have a configured platFetch instance.
|
|
31
|
+
*/
|
|
32
|
+
export function installBridgeWithFetch(platFetch) {
|
|
33
|
+
const originalFetch = window.fetch;
|
|
34
|
+
const originalXHROpen = XMLHttpRequest.prototype.open;
|
|
35
|
+
const originalXHRSend = XMLHttpRequest.prototype.send;
|
|
36
|
+
const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
37
|
+
const blobCache = new Map();
|
|
38
|
+
const blobPending = new Map();
|
|
39
|
+
// ── fetch patch ──────────────────────────────────────────────────────────
|
|
40
|
+
window.fetch = async function (input, init) {
|
|
41
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
42
|
+
if (!shouldIntercept(url))
|
|
43
|
+
return originalFetch.call(window, input, init);
|
|
44
|
+
return platFetch(normalizePath(url), init);
|
|
45
|
+
};
|
|
46
|
+
// ── XHR patch ────────────────────────────────────────────────────────────
|
|
47
|
+
XMLHttpRequest.prototype.open = function (method, url, async_, user, password) {
|
|
48
|
+
this._platUrl = shouldIntercept(url) ? url : null;
|
|
49
|
+
this._platMethod = method;
|
|
50
|
+
this._platHeaders = {};
|
|
51
|
+
if (!this._platUrl) {
|
|
52
|
+
return originalXHROpen.call(this, method, url, async_ ?? true, user ?? null, password ?? null);
|
|
53
|
+
}
|
|
54
|
+
originalXHROpen.call(this, method, 'about:blank', async_ ?? true, user ?? null, password ?? null);
|
|
55
|
+
};
|
|
56
|
+
XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
|
|
57
|
+
if (!this._platUrl) {
|
|
58
|
+
return originalXHRSetRequestHeader.call(this, name, value);
|
|
59
|
+
}
|
|
60
|
+
const headers = (this._platHeaders ??= {});
|
|
61
|
+
const key = name.toLowerCase();
|
|
62
|
+
headers[key] = headers[key] ? `${headers[key]}, ${value}` : value;
|
|
63
|
+
};
|
|
64
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
65
|
+
if (!this._platUrl) {
|
|
66
|
+
return originalXHRSend.call(this, body);
|
|
67
|
+
}
|
|
68
|
+
const xhr = this;
|
|
69
|
+
const path = normalizePath(this._platUrl);
|
|
70
|
+
const method = (this._platMethod || 'GET');
|
|
71
|
+
const headers = (this._platHeaders ?? {});
|
|
72
|
+
platFetch(path, {
|
|
73
|
+
method,
|
|
74
|
+
headers,
|
|
75
|
+
body: coerceXHRBody(body),
|
|
76
|
+
}).then(async (resp) => {
|
|
77
|
+
const text = await resp.text();
|
|
78
|
+
Object.defineProperty(xhr, 'status', { value: resp.status, configurable: true });
|
|
79
|
+
Object.defineProperty(xhr, 'statusText', { value: resp.statusText, configurable: true });
|
|
80
|
+
Object.defineProperty(xhr, 'responseText', { value: text, configurable: true });
|
|
81
|
+
Object.defineProperty(xhr, 'response', { value: text, configurable: true });
|
|
82
|
+
Object.defineProperty(xhr, 'readyState', { value: 4, configurable: true });
|
|
83
|
+
xhr.dispatchEvent(new Event('readystatechange'));
|
|
84
|
+
xhr.dispatchEvent(new Event('load'));
|
|
85
|
+
xhr.dispatchEvent(new Event('loadend'));
|
|
86
|
+
}).catch(() => {
|
|
87
|
+
xhr.dispatchEvent(new Event('error'));
|
|
88
|
+
xhr.dispatchEvent(new Event('loadend'));
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
// ── Resource element interception ────────────────────────────────────────
|
|
92
|
+
const RESOURCE_ATTRS = [
|
|
93
|
+
['img', 'src'], ['script', 'src'], ['link', 'href'],
|
|
94
|
+
['img', 'srcset'],
|
|
95
|
+
['audio', 'src'], ['video', 'src'], ['source', 'src'],
|
|
96
|
+
['source', 'srcset'], ['iframe', 'src'], ['embed', 'src'],
|
|
97
|
+
['object', 'data'],
|
|
98
|
+
];
|
|
99
|
+
/** Fetch a resource path and return its blob URL (cached). */
|
|
100
|
+
async function fetchAsBlobUrl(path, contentType) {
|
|
101
|
+
if (blobCache.has(path))
|
|
102
|
+
return blobCache.get(path);
|
|
103
|
+
if (blobPending.has(path))
|
|
104
|
+
return blobPending.get(path);
|
|
105
|
+
const promise = (async () => {
|
|
106
|
+
try {
|
|
107
|
+
const resp = await platFetch(path);
|
|
108
|
+
if (!resp.ok)
|
|
109
|
+
return null;
|
|
110
|
+
const rawBlob = await resp.blob();
|
|
111
|
+
const blob = contentType && !rawBlob.type
|
|
112
|
+
? new Blob([rawBlob], { type: contentType })
|
|
113
|
+
: rawBlob;
|
|
114
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
115
|
+
blobCache.set(path, blobUrl);
|
|
116
|
+
return blobUrl;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
blobPending.delete(path);
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
blobPending.set(path, promise);
|
|
126
|
+
return promise;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Rewrite CSS url() references to blob URLs.
|
|
130
|
+
* Handles: url(/path), url("path"), url('path'), @import url(...), @import "..."
|
|
131
|
+
*/
|
|
132
|
+
async function rewriteCssUrls(css, basePath) {
|
|
133
|
+
// Collect all url() and @import references
|
|
134
|
+
const urlPattern = /url\(\s*['"]?([^'")]+?)['"]?\s*\)|@import\s+['"]([^'"]+?)['"]/g;
|
|
135
|
+
const replacements = [];
|
|
136
|
+
let m;
|
|
137
|
+
while ((m = urlPattern.exec(css)) !== null) {
|
|
138
|
+
const ref = m[1] ?? m[2];
|
|
139
|
+
if (!ref || !shouldIntercept(ref))
|
|
140
|
+
continue;
|
|
141
|
+
const resolved = normalizePathWithBase(ref, basePath);
|
|
142
|
+
replacements.push({ match: m[0], start: m.index, end: m.index + m[0].length, path: resolved });
|
|
143
|
+
}
|
|
144
|
+
if (replacements.length === 0)
|
|
145
|
+
return css;
|
|
146
|
+
// Fetch all referenced resources in parallel
|
|
147
|
+
const blobUrls = await Promise.all(replacements.map((r) => fetchAsBlobUrl(r.path)));
|
|
148
|
+
// Rebuild CSS with blob URLs
|
|
149
|
+
let result = '';
|
|
150
|
+
let lastEnd = 0;
|
|
151
|
+
for (let i = 0; i < replacements.length; i++) {
|
|
152
|
+
const r = replacements[i];
|
|
153
|
+
const blobUrl = blobUrls[i];
|
|
154
|
+
result += css.slice(lastEnd, r.start);
|
|
155
|
+
if (blobUrl) {
|
|
156
|
+
if (r.match.startsWith('@import')) {
|
|
157
|
+
result += `@import url("${blobUrl}")`;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
result += `url("${blobUrl}")`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
result += r.match; // keep original if fetch failed
|
|
165
|
+
}
|
|
166
|
+
lastEnd = r.end;
|
|
167
|
+
}
|
|
168
|
+
result += css.slice(lastEnd);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
async function rewriteUrl(el, attr) {
|
|
172
|
+
const url = el.getAttribute(attr);
|
|
173
|
+
if (!url || !shouldIntercept(url))
|
|
174
|
+
return;
|
|
175
|
+
const path = normalizePath(url);
|
|
176
|
+
// Special handling for CSS: rewrite url() references inside the CSS
|
|
177
|
+
const rel = (el.getAttribute('rel') ?? '').toLowerCase();
|
|
178
|
+
const isStylesheet = el.tagName.toLowerCase() === 'link' && rel.split(/\s+/).includes('stylesheet');
|
|
179
|
+
if (isStylesheet) {
|
|
180
|
+
const existingBlob = blobCache.get(path);
|
|
181
|
+
if (existingBlob) {
|
|
182
|
+
el.setAttribute(attr, existingBlob);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const resp = await platFetch(path);
|
|
186
|
+
if (!resp.ok)
|
|
187
|
+
return;
|
|
188
|
+
let cssText = await resp.text();
|
|
189
|
+
cssText = await rewriteCssUrls(cssText, path);
|
|
190
|
+
const blob = new Blob([cssText], { type: 'text/css' });
|
|
191
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
192
|
+
blobCache.set(path, blobUrl);
|
|
193
|
+
el.setAttribute(attr, blobUrl);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (attr === 'srcset') {
|
|
197
|
+
const rewritten = await rewriteSrcset(url);
|
|
198
|
+
if (rewritten !== url)
|
|
199
|
+
el.setAttribute(attr, rewritten);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const blobUrl = await fetchAsBlobUrl(path);
|
|
203
|
+
if (blobUrl)
|
|
204
|
+
el.setAttribute(attr, blobUrl);
|
|
205
|
+
}
|
|
206
|
+
async function rewriteSrcset(srcset) {
|
|
207
|
+
const entries = srcset.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
208
|
+
if (entries.length === 0)
|
|
209
|
+
return srcset;
|
|
210
|
+
const rewrittenEntries = [];
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const parts = entry.split(/\s+/);
|
|
213
|
+
const rawUrl = parts[0];
|
|
214
|
+
if (!rawUrl || !shouldIntercept(rawUrl)) {
|
|
215
|
+
rewrittenEntries.push(entry);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const path = normalizePath(rawUrl);
|
|
219
|
+
const blobUrl = await fetchAsBlobUrl(path);
|
|
220
|
+
rewrittenEntries.push(blobUrl ? [blobUrl, ...parts.slice(1)].join(' ') : entry);
|
|
221
|
+
}
|
|
222
|
+
return rewrittenEntries.join(', ');
|
|
223
|
+
}
|
|
224
|
+
/** Also rewrite inline <style> elements that contain url() references */
|
|
225
|
+
async function rewriteInlineStyle(el) {
|
|
226
|
+
if (!el.textContent)
|
|
227
|
+
return;
|
|
228
|
+
const rewritten = await rewriteCssUrls(el.textContent, '/');
|
|
229
|
+
if (rewritten !== el.textContent) {
|
|
230
|
+
el.textContent = rewritten;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function processElement(el) {
|
|
234
|
+
const tag = el.tagName.toLowerCase();
|
|
235
|
+
for (const [t, attr] of RESOURCE_ATTRS) {
|
|
236
|
+
if (tag === t && el.hasAttribute(attr)) {
|
|
237
|
+
void rewriteUrl(el, attr);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Inline <style> url() rewriting
|
|
241
|
+
if (tag === 'style' && el instanceof HTMLStyleElement) {
|
|
242
|
+
void rewriteInlineStyle(el);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function processExisting() {
|
|
246
|
+
for (const [tag, attr] of RESOURCE_ATTRS) {
|
|
247
|
+
document.querySelectorAll(`${tag}[${attr}]`).forEach((el) => void rewriteUrl(el, attr));
|
|
248
|
+
}
|
|
249
|
+
// Rewrite existing inline styles
|
|
250
|
+
document.querySelectorAll('style').forEach((el) => {
|
|
251
|
+
if (el instanceof HTMLStyleElement)
|
|
252
|
+
void rewriteInlineStyle(el);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
// ── Worker interception ──────────────────────────────────────────────────
|
|
256
|
+
const OriginalWorker = window.Worker;
|
|
257
|
+
window.Worker = class PlatWorker extends OriginalWorker {
|
|
258
|
+
constructor(scriptURL, options) {
|
|
259
|
+
const url = typeof scriptURL === 'string' ? scriptURL : scriptURL.href;
|
|
260
|
+
if (shouldIntercept(url)) {
|
|
261
|
+
const path = normalizePath(url);
|
|
262
|
+
const isModule = options?.type === 'module';
|
|
263
|
+
const loaderCode = isModule
|
|
264
|
+
? `
|
|
265
|
+
(async () => {
|
|
266
|
+
const resp = await fetch(${JSON.stringify(path)});
|
|
267
|
+
if (!resp.ok) throw new Error('Failed to load worker script: ' + ${JSON.stringify(path)});
|
|
268
|
+
const code = await resp.text();
|
|
269
|
+
const blobUrl = URL.createObjectURL(new Blob([code], { type: 'text/javascript' }));
|
|
270
|
+
try { await import(blobUrl); } finally { URL.revokeObjectURL(blobUrl); }
|
|
271
|
+
})().catch((err) => { throw err; });
|
|
272
|
+
`
|
|
273
|
+
: `
|
|
274
|
+
(async () => {
|
|
275
|
+
const resp = await fetch(${JSON.stringify(path)});
|
|
276
|
+
if (!resp.ok) throw new Error('Failed to load worker script: ' + ${JSON.stringify(path)});
|
|
277
|
+
const code = await resp.text();
|
|
278
|
+
const blobUrl = URL.createObjectURL(new Blob([code], { type: 'text/javascript' }));
|
|
279
|
+
try { importScripts(blobUrl); } finally { URL.revokeObjectURL(blobUrl); }
|
|
280
|
+
})().catch((err) => { throw err; });
|
|
281
|
+
`;
|
|
282
|
+
const loaderBlob = new Blob([loaderCode], { type: 'text/javascript' });
|
|
283
|
+
const loaderUrl = URL.createObjectURL(loaderBlob);
|
|
284
|
+
super(loaderUrl, options);
|
|
285
|
+
URL.revokeObjectURL(loaderUrl);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
super(scriptURL, options);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
// ── navigator.sendBeacon interception ────────────────────────────────────
|
|
293
|
+
const originalSendBeacon = navigator.sendBeacon?.bind(navigator);
|
|
294
|
+
if (originalSendBeacon) {
|
|
295
|
+
navigator.sendBeacon = function (url, data) {
|
|
296
|
+
if (shouldIntercept(url)) {
|
|
297
|
+
const path = normalizePath(url);
|
|
298
|
+
void platFetch(path, {
|
|
299
|
+
method: 'POST',
|
|
300
|
+
body: data,
|
|
301
|
+
});
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return originalSendBeacon(url, data);
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// ── EventSource interception ─────────────────────────────────────────────
|
|
308
|
+
// EventSource over plat doesn't map perfectly (SSE is streaming),
|
|
309
|
+
// but we can intercept construction and poll via fetch
|
|
310
|
+
const OriginalEventSource = window.EventSource;
|
|
311
|
+
window.EventSource = class PlatEventSource extends EventTarget {
|
|
312
|
+
abortController;
|
|
313
|
+
url;
|
|
314
|
+
readyState = 0;
|
|
315
|
+
withCredentials = false;
|
|
316
|
+
onopen = null;
|
|
317
|
+
onmessage = null;
|
|
318
|
+
onerror = null;
|
|
319
|
+
static CONNECTING = 0;
|
|
320
|
+
static OPEN = 1;
|
|
321
|
+
static CLOSED = 2;
|
|
322
|
+
CONNECTING = 0;
|
|
323
|
+
OPEN = 1;
|
|
324
|
+
CLOSED = 2;
|
|
325
|
+
constructor(url, init) {
|
|
326
|
+
super();
|
|
327
|
+
this.url = typeof url === 'string' ? url : url.href;
|
|
328
|
+
if (shouldIntercept(this.url)) {
|
|
329
|
+
const path = normalizePath(this.url);
|
|
330
|
+
this.readyState = 0;
|
|
331
|
+
this.abortController = new AbortController();
|
|
332
|
+
setTimeout(() => {
|
|
333
|
+
this.readyState = 1;
|
|
334
|
+
const openEvent = new Event('open');
|
|
335
|
+
this.onopen?.(openEvent);
|
|
336
|
+
this.dispatchEvent(openEvent);
|
|
337
|
+
platFetch(path, {
|
|
338
|
+
headers: { accept: 'text/event-stream' },
|
|
339
|
+
signal: this.abortController.signal,
|
|
340
|
+
}).then(async (resp) => {
|
|
341
|
+
if (!resp.ok)
|
|
342
|
+
throw new Error(`EventSource request failed: ${resp.status}`);
|
|
343
|
+
if (!resp.body) {
|
|
344
|
+
const text = await resp.text();
|
|
345
|
+
const msgEvent = new MessageEvent('message', { data: text });
|
|
346
|
+
this.onmessage?.(msgEvent);
|
|
347
|
+
this.dispatchEvent(msgEvent);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const reader = resp.body.getReader();
|
|
351
|
+
const decoder = new TextDecoder();
|
|
352
|
+
let buffer = '';
|
|
353
|
+
let eventName = 'message';
|
|
354
|
+
let eventData = [];
|
|
355
|
+
const flushEvent = () => {
|
|
356
|
+
if (eventData.length === 0)
|
|
357
|
+
return;
|
|
358
|
+
const payload = eventData.join('\n');
|
|
359
|
+
const evt = new MessageEvent(eventName || 'message', { data: payload });
|
|
360
|
+
this.onmessage?.(evt);
|
|
361
|
+
this.dispatchEvent(evt);
|
|
362
|
+
eventName = 'message';
|
|
363
|
+
eventData = [];
|
|
364
|
+
};
|
|
365
|
+
while (true) {
|
|
366
|
+
const { done, value } = await reader.read();
|
|
367
|
+
if (done)
|
|
368
|
+
break;
|
|
369
|
+
buffer += decoder.decode(value, { stream: true });
|
|
370
|
+
let splitIndex = buffer.indexOf('\n');
|
|
371
|
+
while (splitIndex >= 0) {
|
|
372
|
+
const line = buffer.slice(0, splitIndex).replace(/\r$/, '');
|
|
373
|
+
buffer = buffer.slice(splitIndex + 1);
|
|
374
|
+
if (line === '') {
|
|
375
|
+
flushEvent();
|
|
376
|
+
}
|
|
377
|
+
else if (line.startsWith('event:')) {
|
|
378
|
+
eventName = line.slice(6).trim() || 'message';
|
|
379
|
+
}
|
|
380
|
+
else if (line.startsWith('data:')) {
|
|
381
|
+
eventData.push(line.slice(5).trimStart());
|
|
382
|
+
}
|
|
383
|
+
splitIndex = buffer.indexOf('\n');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (buffer.trim().length > 0) {
|
|
387
|
+
if (buffer.startsWith('data:'))
|
|
388
|
+
eventData.push(buffer.slice(5).trimStart());
|
|
389
|
+
}
|
|
390
|
+
flushEvent();
|
|
391
|
+
}).catch(() => {
|
|
392
|
+
this.readyState = 2;
|
|
393
|
+
const errEvent = new Event('error');
|
|
394
|
+
this.onerror?.(errEvent);
|
|
395
|
+
this.dispatchEvent(errEvent);
|
|
396
|
+
});
|
|
397
|
+
}, 0);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// Fall back to real EventSource — but we can't extend it easily,
|
|
401
|
+
// so just return a real one
|
|
402
|
+
return new OriginalEventSource(url, init);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
close() {
|
|
406
|
+
this.abortController?.abort();
|
|
407
|
+
this.readyState = 2;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
// ── <a> click interception (for SPA-like navigation within the bridge) ──
|
|
411
|
+
const onDocumentClick = (e) => {
|
|
412
|
+
if (e.defaultPrevented)
|
|
413
|
+
return;
|
|
414
|
+
if (e.button !== 0)
|
|
415
|
+
return;
|
|
416
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
|
|
417
|
+
return;
|
|
418
|
+
const anchor = e.target.closest?.('a[href]');
|
|
419
|
+
if (!anchor)
|
|
420
|
+
return;
|
|
421
|
+
if (anchor.target && anchor.target !== '_self')
|
|
422
|
+
return;
|
|
423
|
+
const href = anchor.getAttribute('href');
|
|
424
|
+
if (!href || !shouldIntercept(href))
|
|
425
|
+
return;
|
|
426
|
+
// Prevent default navigation, instead fetch the page through plat
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
const path = normalizePath(href);
|
|
429
|
+
platFetch(path).then(async (resp) => {
|
|
430
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
431
|
+
if (contentType.includes('text/html')) {
|
|
432
|
+
const html = await resp.text();
|
|
433
|
+
// Rewrite the document body (SPA-style navigation)
|
|
434
|
+
document.open();
|
|
435
|
+
document.write(html);
|
|
436
|
+
document.close();
|
|
437
|
+
// Re-process all elements after rewrite
|
|
438
|
+
processExisting();
|
|
439
|
+
history.pushState(null, '', href);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Non-HTML: download or open in new tab
|
|
443
|
+
const blob = await resp.blob();
|
|
444
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
445
|
+
window.open(blobUrl, '_blank');
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
document.addEventListener('click', onDocumentClick, true);
|
|
450
|
+
// ── Form submission interception ──────────────────────────────────────────
|
|
451
|
+
const onFormSubmit = (e) => {
|
|
452
|
+
const form = e.target;
|
|
453
|
+
if (!form || !(form instanceof HTMLFormElement))
|
|
454
|
+
return;
|
|
455
|
+
const action = form.getAttribute('action') || window.location.pathname;
|
|
456
|
+
if (!shouldIntercept(action))
|
|
457
|
+
return;
|
|
458
|
+
e.preventDefault();
|
|
459
|
+
const method = (form.getAttribute('method') || 'GET').toUpperCase();
|
|
460
|
+
const path = normalizePath(action);
|
|
461
|
+
let body = undefined;
|
|
462
|
+
const enctype = (form.getAttribute('enctype') || 'application/x-www-form-urlencoded').toLowerCase();
|
|
463
|
+
// For GET, append form data to URL
|
|
464
|
+
if (method === 'GET') {
|
|
465
|
+
const formData = new FormData(form);
|
|
466
|
+
const params = new URLSearchParams(formData);
|
|
467
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
468
|
+
const pathWithParams = `${path}${separator}${params.toString()}`;
|
|
469
|
+
platFetch(pathWithParams).then(async (resp) => {
|
|
470
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
471
|
+
if (contentType.includes('text/html')) {
|
|
472
|
+
const html = await resp.text();
|
|
473
|
+
document.open();
|
|
474
|
+
document.write(html);
|
|
475
|
+
document.close();
|
|
476
|
+
processExisting();
|
|
477
|
+
history.pushState(null, '', pathWithParams);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const blob = await resp.blob();
|
|
481
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
482
|
+
window.open(blobUrl, '_blank');
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
// For POST/PUT/PATCH, send form data as body
|
|
488
|
+
body = new FormData(form);
|
|
489
|
+
platFetch(path, { method, body }).then(async (resp) => {
|
|
490
|
+
const contentType = resp.headers.get('content-type') || '';
|
|
491
|
+
if (contentType.includes('text/html')) {
|
|
492
|
+
const html = await resp.text();
|
|
493
|
+
document.open();
|
|
494
|
+
document.write(html);
|
|
495
|
+
document.close();
|
|
496
|
+
processExisting();
|
|
497
|
+
history.pushState(null, '', path);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
const blob = await resp.blob();
|
|
501
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
502
|
+
window.open(blobUrl, '_blank');
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
document.addEventListener('submit', onFormSubmit, true);
|
|
508
|
+
const observer = new MutationObserver((mutations) => {
|
|
509
|
+
for (const mutation of mutations) {
|
|
510
|
+
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
511
|
+
const node = mutation.addedNodes[i];
|
|
512
|
+
if (node instanceof Element) {
|
|
513
|
+
processElement(node);
|
|
514
|
+
node.querySelectorAll('*').forEach((child) => processElement(child));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (mutation.type === 'attributes' && mutation.target instanceof Element) {
|
|
518
|
+
processElement(mutation.target);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
if (document.readyState === 'loading') {
|
|
523
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
524
|
+
processExisting();
|
|
525
|
+
observer.observe(document.documentElement, {
|
|
526
|
+
childList: true, subtree: true, attributes: true,
|
|
527
|
+
attributeFilter: ['src', 'href', 'data', 'srcset'],
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
processExisting();
|
|
533
|
+
observer.observe(document.documentElement, {
|
|
534
|
+
childList: true, subtree: true, attributes: true,
|
|
535
|
+
attributeFilter: ['src', 'href', 'data', 'srcset'],
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
restore() {
|
|
540
|
+
window.fetch = originalFetch;
|
|
541
|
+
XMLHttpRequest.prototype.open = originalXHROpen;
|
|
542
|
+
XMLHttpRequest.prototype.send = originalXHRSend;
|
|
543
|
+
XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;
|
|
544
|
+
window.Worker = OriginalWorker;
|
|
545
|
+
if (originalSendBeacon)
|
|
546
|
+
navigator.sendBeacon = originalSendBeacon;
|
|
547
|
+
window.EventSource = OriginalEventSource;
|
|
548
|
+
document.removeEventListener('click', onDocumentClick, true);
|
|
549
|
+
document.removeEventListener('submit', onFormSubmit, true);
|
|
550
|
+
observer.disconnect();
|
|
551
|
+
for (const blobUrl of blobCache.values())
|
|
552
|
+
URL.revokeObjectURL(blobUrl);
|
|
553
|
+
blobCache.clear();
|
|
554
|
+
blobPending.clear();
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// ── Iframe mode (self-contained script) ──────────────────────────────────────
|
|
559
|
+
/**
|
|
560
|
+
* Generate the bridge script source code for injection into iframes.
|
|
561
|
+
* The returned string is self-contained JS that patches fetch/XHR/DOM
|
|
562
|
+
* resource loading to route through postMessage to the parent frame.
|
|
563
|
+
*/
|
|
564
|
+
export function generateBridgeScript() {
|
|
565
|
+
return `(${iframeBridgeRuntime.toString()})();`;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Self-contained runtime for iframe mode.
|
|
569
|
+
* Serialized and injected — must not reference external variables.
|
|
570
|
+
*/
|
|
571
|
+
function iframeBridgeRuntime() {
|
|
572
|
+
const _originalFetch = window.fetch.bind(window);
|
|
573
|
+
const _originalXHROpen = XMLHttpRequest.prototype.open;
|
|
574
|
+
const _originalXHRSend = XMLHttpRequest.prototype.send;
|
|
575
|
+
let _counter = 0;
|
|
576
|
+
const _pending = new Map();
|
|
577
|
+
const _blobCache = new Map();
|
|
578
|
+
const _blobPending = new Map();
|
|
579
|
+
function shouldIntercept(url) {
|
|
580
|
+
if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('javascript:'))
|
|
581
|
+
return false;
|
|
582
|
+
if (url.startsWith('/') || !url.includes('://'))
|
|
583
|
+
return true;
|
|
584
|
+
if (url.startsWith('css://'))
|
|
585
|
+
return true;
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
function normalizePath(url) {
|
|
589
|
+
if (url.startsWith('css://')) {
|
|
590
|
+
try {
|
|
591
|
+
return new URL(url.replace('css://', 'http://')).pathname;
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
return url;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (url.startsWith('/'))
|
|
598
|
+
return url;
|
|
599
|
+
try {
|
|
600
|
+
return new URL(url, window.location.href).pathname;
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
return '/' + url;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function bridgeFetch(method, path, headers, body) {
|
|
607
|
+
const id = `bridge-${++_counter}`;
|
|
608
|
+
return new Promise((resolve, reject) => {
|
|
609
|
+
_pending.set(id, { resolve, reject });
|
|
610
|
+
window.parent.postMessage({ type: 'plat-fetch', id, method, path, headers, body }, '*');
|
|
611
|
+
setTimeout(() => {
|
|
612
|
+
if (_pending.has(id)) {
|
|
613
|
+
_pending.delete(id);
|
|
614
|
+
reject(new Error(`Timeout: ${method} ${path}`));
|
|
615
|
+
}
|
|
616
|
+
}, 30000);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
window.addEventListener('message', (e) => {
|
|
620
|
+
if (e.source !== window.parent)
|
|
621
|
+
return;
|
|
622
|
+
const data = e.data;
|
|
623
|
+
if (!data || data.type !== 'plat-fetch-response' || typeof data.id !== 'string')
|
|
624
|
+
return;
|
|
625
|
+
const p = _pending.get(data.id);
|
|
626
|
+
if (!p)
|
|
627
|
+
return;
|
|
628
|
+
_pending.delete(data.id);
|
|
629
|
+
const body = data.body;
|
|
630
|
+
const isBodySupported = typeof body === 'string' || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
|
|
631
|
+
if (!isBodySupported) {
|
|
632
|
+
p.reject(new Error('Invalid bridge response body'));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
p.resolve(data);
|
|
636
|
+
});
|
|
637
|
+
function decodeBridgeBody(body) {
|
|
638
|
+
if (body instanceof ArrayBuffer)
|
|
639
|
+
return new Uint8Array(body);
|
|
640
|
+
if (ArrayBuffer.isView(body))
|
|
641
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
642
|
+
if (typeof body === 'string') {
|
|
643
|
+
const normalized = body.replace(/-/g, '+').replace(/_/g, '/');
|
|
644
|
+
const padLength = (4 - (normalized.length % 4)) % 4;
|
|
645
|
+
const padded = normalized + '='.repeat(padLength);
|
|
646
|
+
try {
|
|
647
|
+
const s = atob(padded);
|
|
648
|
+
const a = new Uint8Array(s.length);
|
|
649
|
+
for (let i = 0; i < s.length; i++)
|
|
650
|
+
a[i] = s.charCodeAt(i);
|
|
651
|
+
return a;
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Some hosts may still send plain text payloads instead of base64.
|
|
655
|
+
return new TextEncoder().encode(body);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
throw new Error('Unsupported bridge response body type');
|
|
659
|
+
}
|
|
660
|
+
// Patch fetch
|
|
661
|
+
window.fetch = async function (input, init) {
|
|
662
|
+
const isRequestInput = typeof Request !== 'undefined' && input instanceof Request;
|
|
663
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
664
|
+
if (!shouldIntercept(url))
|
|
665
|
+
return _originalFetch(input, init);
|
|
666
|
+
const method = (init?.method ?? (isRequestInput ? input.method : undefined) ?? 'GET').toUpperCase();
|
|
667
|
+
const path = normalizePath(url);
|
|
668
|
+
const headers = {};
|
|
669
|
+
const headerSource = init?.headers ?? (isRequestInput ? input.headers : undefined);
|
|
670
|
+
if (headerSource) {
|
|
671
|
+
const h = new Headers(headerSource);
|
|
672
|
+
h.forEach((v, k) => { headers[k] = v; });
|
|
673
|
+
}
|
|
674
|
+
const body = init?.body ? (typeof init.body === 'string' ? init.body : null) : null;
|
|
675
|
+
const resp = await bridgeFetch(method, path, headers, body);
|
|
676
|
+
const bytes = decodeBridgeBody(resp.body);
|
|
677
|
+
const rh = { ...resp.headers };
|
|
678
|
+
if (resp.contentType)
|
|
679
|
+
rh['content-type'] = resp.contentType;
|
|
680
|
+
return new Response(bytes, { status: resp.status, statusText: resp.statusText, headers: rh });
|
|
681
|
+
};
|
|
682
|
+
// Patch XHR
|
|
683
|
+
XMLHttpRequest.prototype.open = function (method, url, async_, user, password) {
|
|
684
|
+
this._pUrl = shouldIntercept(url) ? url : null;
|
|
685
|
+
this._pMethod = method;
|
|
686
|
+
if (!this._pUrl)
|
|
687
|
+
return _originalXHROpen.call(this, method, url, async_ ?? true, user ?? null, password ?? null);
|
|
688
|
+
_originalXHROpen.call(this, method, 'about:blank', async_ ?? true, user ?? null, password ?? null);
|
|
689
|
+
};
|
|
690
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
691
|
+
if (!this._pUrl)
|
|
692
|
+
return _originalXHRSend.call(this, body);
|
|
693
|
+
const xhr = this, path = normalizePath(this._pUrl), method = this._pMethod || 'GET';
|
|
694
|
+
bridgeFetch(method, path, {}, body ? String(body) : null).then((r) => {
|
|
695
|
+
const text = new TextDecoder().decode(decodeBridgeBody(r.body));
|
|
696
|
+
Object.defineProperty(xhr, 'status', { value: r.status, configurable: true });
|
|
697
|
+
Object.defineProperty(xhr, 'statusText', { value: r.statusText, configurable: true });
|
|
698
|
+
Object.defineProperty(xhr, 'responseText', { value: text, configurable: true });
|
|
699
|
+
Object.defineProperty(xhr, 'response', { value: text, configurable: true });
|
|
700
|
+
Object.defineProperty(xhr, 'readyState', { value: 4, configurable: true });
|
|
701
|
+
xhr.dispatchEvent(new Event('readystatechange'));
|
|
702
|
+
xhr.dispatchEvent(new Event('load'));
|
|
703
|
+
xhr.dispatchEvent(new Event('loadend'));
|
|
704
|
+
}).catch(() => { xhr.dispatchEvent(new Event('error')); xhr.dispatchEvent(new Event('loadend')); });
|
|
705
|
+
};
|
|
706
|
+
// Resource element rewriting
|
|
707
|
+
const ATTRS = [
|
|
708
|
+
['img', 'src'], ['script', 'src'], ['link', 'href'], ['audio', 'src'],
|
|
709
|
+
['video', 'src'], ['source', 'src'], ['source', 'srcset'],
|
|
710
|
+
['iframe', 'src'], ['embed', 'src'], ['object', 'data'],
|
|
711
|
+
];
|
|
712
|
+
async function rewrite(el, attr) {
|
|
713
|
+
const url = el.getAttribute(attr);
|
|
714
|
+
if (!url || !shouldIntercept(url))
|
|
715
|
+
return;
|
|
716
|
+
const path = normalizePath(url);
|
|
717
|
+
if (_blobCache.has(path)) {
|
|
718
|
+
el.setAttribute(attr, _blobCache.get(path));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (_blobPending.has(path)) {
|
|
722
|
+
el.setAttribute(attr, await _blobPending.get(path));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const p = (async () => {
|
|
726
|
+
try {
|
|
727
|
+
const r = await bridgeFetch('GET', path, {}, null);
|
|
728
|
+
if (!r.ok)
|
|
729
|
+
return url;
|
|
730
|
+
const blob = new Blob([decodeBridgeBody(r.body)], { type: r.contentType || 'application/octet-stream' });
|
|
731
|
+
const bu = URL.createObjectURL(blob);
|
|
732
|
+
_blobCache.set(path, bu);
|
|
733
|
+
return bu;
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return url;
|
|
737
|
+
}
|
|
738
|
+
finally {
|
|
739
|
+
_blobPending.delete(path);
|
|
740
|
+
}
|
|
741
|
+
})();
|
|
742
|
+
_blobPending.set(path, p);
|
|
743
|
+
el.setAttribute(attr, await p);
|
|
744
|
+
}
|
|
745
|
+
function processEl(el) {
|
|
746
|
+
const t = el.tagName.toLowerCase();
|
|
747
|
+
for (const [tag, attr] of ATTRS) {
|
|
748
|
+
if (t === tag && el.hasAttribute(attr))
|
|
749
|
+
void rewrite(el, attr);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function processAll() {
|
|
753
|
+
for (const [tag, attr] of ATTRS) {
|
|
754
|
+
document.querySelectorAll(`${tag}[${attr}]`).forEach((el) => void rewrite(el, attr));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const obs = new MutationObserver((muts) => {
|
|
758
|
+
for (const m of muts) {
|
|
759
|
+
for (let i = 0; i < m.addedNodes.length; i++) {
|
|
760
|
+
const n = m.addedNodes[i];
|
|
761
|
+
if (n instanceof Element) {
|
|
762
|
+
processEl(n);
|
|
763
|
+
n.querySelectorAll('*').forEach((c) => processEl(c));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (m.type === 'attributes' && m.target instanceof Element)
|
|
767
|
+
processEl(m.target);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
const startObs = () => {
|
|
771
|
+
processAll();
|
|
772
|
+
obs.observe(document.documentElement, {
|
|
773
|
+
childList: true, subtree: true, attributes: true,
|
|
774
|
+
attributeFilter: ['src', 'href', 'data', 'srcset'],
|
|
775
|
+
});
|
|
776
|
+
};
|
|
777
|
+
if (document.readyState === 'loading')
|
|
778
|
+
document.addEventListener('DOMContentLoaded', startObs);
|
|
779
|
+
else
|
|
780
|
+
startObs();
|
|
781
|
+
// Form submission interception for iframe
|
|
782
|
+
window.addEventListener('submit', (e) => {
|
|
783
|
+
const form = e.target;
|
|
784
|
+
if (!form || !(form instanceof HTMLFormElement))
|
|
785
|
+
return;
|
|
786
|
+
const action = form.getAttribute('action') || window.location.pathname;
|
|
787
|
+
if (action.startsWith('data:') || action.startsWith('blob:') || action.startsWith('javascript:'))
|
|
788
|
+
return;
|
|
789
|
+
if (action.startsWith('/') || !action.includes('://') || action.startsWith('css://')) {
|
|
790
|
+
e.preventDefault();
|
|
791
|
+
const method = (form.getAttribute('method') || 'GET').toUpperCase();
|
|
792
|
+
const path = normalizePath(action);
|
|
793
|
+
const fd = new FormData(form);
|
|
794
|
+
const body = new URLSearchParams(fd).toString();
|
|
795
|
+
bridgeFetch(method, path, {}, body);
|
|
796
|
+
}
|
|
797
|
+
}, true);
|
|
798
|
+
// <a> click interception — delegate navigation to parent frame instead of letting
|
|
799
|
+
// the browser attempt a native navigation (which fails for srcdoc-loaded iframes).
|
|
800
|
+
document.addEventListener('click', function (e) {
|
|
801
|
+
if (e.defaultPrevented || e.button !== 0)
|
|
802
|
+
return;
|
|
803
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
|
|
804
|
+
return;
|
|
805
|
+
const anchor = e.target.closest?.('a[href]');
|
|
806
|
+
if (!anchor)
|
|
807
|
+
return;
|
|
808
|
+
const target = anchor.target;
|
|
809
|
+
if (target && target !== '_self')
|
|
810
|
+
return;
|
|
811
|
+
const href = anchor.getAttribute('href');
|
|
812
|
+
if (!href || !shouldIntercept(href))
|
|
813
|
+
return;
|
|
814
|
+
e.preventDefault();
|
|
815
|
+
const path = normalizePath(href);
|
|
816
|
+
// Tell the parent BrowserView to navigate to this path
|
|
817
|
+
window.parent.postMessage({ type: 'plat-navigate', path }, '*');
|
|
818
|
+
}, true);
|
|
819
|
+
}
|
|
820
|
+
// ── Shared helpers ───────────────────────────────────────────────────────────
|
|
821
|
+
function shouldIntercept(url) {
|
|
822
|
+
if (url.startsWith('data:') || url.startsWith('blob:') || url.startsWith('javascript:'))
|
|
823
|
+
return false;
|
|
824
|
+
if (url.startsWith('/') || !url.includes('://'))
|
|
825
|
+
return true;
|
|
826
|
+
if (url.startsWith('css://'))
|
|
827
|
+
return true;
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
function normalizePath(url) {
|
|
831
|
+
if (url.startsWith('css://')) {
|
|
832
|
+
try {
|
|
833
|
+
const parsed = new URL(url.replace('css://', 'http://'));
|
|
834
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return url;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (url.startsWith('/'))
|
|
841
|
+
return url;
|
|
842
|
+
try {
|
|
843
|
+
const parsed = new URL(url, window.location.href);
|
|
844
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return '/' + url;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function normalizePathWithBase(url, basePath) {
|
|
851
|
+
if (url.startsWith('/'))
|
|
852
|
+
return url;
|
|
853
|
+
const baseDir = basePath.replace(/\/[^/]*$/, '/');
|
|
854
|
+
try {
|
|
855
|
+
const parsed = new URL(url, `http://localhost${baseDir}`);
|
|
856
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return normalizePath(url);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function coerceXHRBody(body) {
|
|
863
|
+
if (body == null)
|
|
864
|
+
return undefined;
|
|
865
|
+
if (typeof body === 'string')
|
|
866
|
+
return body;
|
|
867
|
+
if (body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body)
|
|
868
|
+
|| body instanceof URLSearchParams || body instanceof FormData || body instanceof ReadableStream) {
|
|
869
|
+
return body;
|
|
870
|
+
}
|
|
871
|
+
if (body instanceof Document) {
|
|
872
|
+
return new XMLSerializer().serializeToString(body);
|
|
873
|
+
}
|
|
874
|
+
return String(body);
|
|
875
|
+
}
|
|
876
|
+
//# sourceMappingURL=plat-bridge.js.map
|