@scelar/nodepod 1.0.0 → 1.0.1

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/dist/__sw__.js ADDED
@@ -0,0 +1,642 @@
1
+ /**
2
+ * Nodepod Service Worker — proxies requests to virtual servers.
3
+ * Version: 2 (cross-origin passthrough + prefix stripping)
4
+ *
5
+ * Intercepts:
6
+ * /__virtual__/{port}/{path} — virtual server API
7
+ * /__preview__/{port}/{path} — preview iframe navigation
8
+ * Any request from a client loaded via /__preview__/ — module imports etc.
9
+ *
10
+ * When an iframe navigates to /__preview__/{port}/, the SW records the
11
+ * resulting clientId. All subsequent requests from that client (including
12
+ * ES module imports like /@react-refresh) are intercepted and routed
13
+ * through the virtual server.
14
+ */
15
+
16
+ const SW_VERSION = 4;
17
+
18
+ let port = null;
19
+ let nextId = 1;
20
+ const pending = new Map();
21
+
22
+ // Maps clientId -> serverPort for preview iframes
23
+ const previewClients = new Map();
24
+
25
+ // User-injected script that runs before any page content in preview iframes.
26
+ // Set via postMessage({ type: "set-preview-script", script: "..." }) from main thread.
27
+ let previewScript = null;
28
+
29
+ // Watermark badge shown in preview iframes. On by default.
30
+ let watermarkEnabled = true;
31
+
32
+ // Standard MIME types by file extension — used as a safety net when
33
+ // the virtual server returns text/html (SPA fallback) or omits Content-Type
34
+ // for paths that are clearly not HTML.
35
+ const MIME_TYPES = {
36
+ ".js": "application/javascript",
37
+ ".mjs": "application/javascript",
38
+ ".cjs": "application/javascript",
39
+ ".ts": "application/javascript",
40
+ ".tsx": "application/javascript",
41
+ ".jsx": "application/javascript",
42
+ ".css": "text/css",
43
+ ".json": "application/json",
44
+ ".map": "application/json",
45
+ ".svg": "image/svg+xml",
46
+ ".png": "image/png",
47
+ ".jpg": "image/jpeg",
48
+ ".jpeg": "image/jpeg",
49
+ ".gif": "image/gif",
50
+ ".webp": "image/webp",
51
+ ".avif": "image/avif",
52
+ ".ico": "image/x-icon",
53
+ ".woff": "font/woff",
54
+ ".woff2": "font/woff2",
55
+ ".ttf": "font/ttf",
56
+ ".otf": "font/otf",
57
+ ".eot": "application/vnd.ms-fontobject",
58
+ ".wasm": "application/wasm",
59
+ ".mp4": "video/mp4",
60
+ ".webm": "video/webm",
61
+ ".mp3": "audio/mpeg",
62
+ ".ogg": "audio/ogg",
63
+ ".wav": "audio/wav",
64
+ ".txt": "text/plain",
65
+ ".xml": "application/xml",
66
+ ".pdf": "application/pdf",
67
+ ".yaml": "text/yaml",
68
+ ".yml": "text/yaml",
69
+ ".md": "text/markdown",
70
+ };
71
+
72
+ /**
73
+ * Infer correct MIME type for a response based on the request path.
74
+ * When a server's SPA fallback serves index.html (text/html) for paths that
75
+ * are clearly not HTML (e.g. .js, .css, .json files), the Content-Type is
76
+ * wrong. This corrects it based purely on the file extension in the URL.
77
+ */
78
+ function inferMimeType(path, responseHeaders) {
79
+ const ct =
80
+ responseHeaders["content-type"] || responseHeaders["Content-Type"] || "";
81
+
82
+ // If the server already set a non-HTML Content-Type, trust it
83
+ if (ct && !ct.includes("text/html")) {
84
+ return null; // no override needed
85
+ }
86
+
87
+ // Strip query string and hash for extension detection
88
+ const cleanPath = path.split("?")[0].split("#")[0];
89
+ const lastDot = cleanPath.lastIndexOf(".");
90
+ const ext = lastDot >= 0 ? cleanPath.slice(lastDot).toLowerCase() : "";
91
+
92
+ // Only override if the path has a known non-HTML extension
93
+ if (ext && MIME_TYPES[ext]) {
94
+ return MIME_TYPES[ext];
95
+ }
96
+
97
+ return null; // no override
98
+ }
99
+
100
+ // ── Lifecycle ──
101
+
102
+ self.addEventListener("install", () => {
103
+ self.skipWaiting();
104
+ });
105
+ self.addEventListener("activate", (event) => {
106
+ event.waitUntil(self.clients.claim());
107
+ });
108
+
109
+ // ── Message handling ──
110
+
111
+ self.addEventListener("message", (event) => {
112
+ const data = event.data;
113
+ if (data?.type === "init" && data.port) {
114
+ port = data.port;
115
+ port.onmessage = onPortMessage;
116
+ }
117
+ // Allow main thread to register/unregister preview clients
118
+ if (data?.type === "register-preview") {
119
+ previewClients.set(data.clientId, data.serverPort);
120
+ }
121
+ if (data?.type === "unregister-preview") {
122
+ previewClients.delete(data.clientId);
123
+ }
124
+ if (data?.type === "set-preview-script") {
125
+ previewScript = data.script ?? null;
126
+ }
127
+ if (data?.type === "set-watermark") {
128
+ watermarkEnabled = !!data.enabled;
129
+ }
130
+ });
131
+
132
+ function onPortMessage(event) {
133
+ const msg = event.data;
134
+ if (msg.type === "response" && pending.has(msg.id)) {
135
+ const { resolve, reject } = pending.get(msg.id);
136
+ pending.delete(msg.id);
137
+ if (msg.error) reject(new Error(msg.error));
138
+ else resolve(msg.data);
139
+ }
140
+ }
141
+
142
+ // ── Fetch interception ──
143
+
144
+ self.addEventListener("fetch", (event) => {
145
+ const url = new URL(event.request.url);
146
+
147
+ // 1. Explicit /__virtual__/{port}/{path}
148
+ const virtualMatch = url.pathname.match(/^\/__virtual__\/(\d+)(\/.*)?$/);
149
+ if (virtualMatch) {
150
+ const serverPort = parseInt(virtualMatch[1], 10);
151
+ const path = (virtualMatch[2] || "/") + url.search;
152
+ event.respondWith(proxyToVirtualServer(event.request, serverPort, path));
153
+ return;
154
+ }
155
+
156
+ // 2. Explicit /__preview__/{port}/{path} — navigation or subresource
157
+ const previewMatch = url.pathname.match(/^\/__preview__\/(\d+)(\/.*)?$/);
158
+ if (previewMatch) {
159
+ const serverPort = parseInt(previewMatch[1], 10);
160
+ const path = (previewMatch[2] || "/") + url.search;
161
+
162
+ // Track the resulting client (for navigation requests) or current client
163
+ if (event.request.mode === "navigate") {
164
+ event.respondWith(
165
+ (async () => {
166
+ // resultingClientId is the client that will be created by this navigation
167
+ if (event.resultingClientId) {
168
+ previewClients.set(event.resultingClientId, serverPort);
169
+ }
170
+ return proxyToVirtualServer(event.request, serverPort, path);
171
+ })(),
172
+ );
173
+ } else {
174
+ event.respondWith(proxyToVirtualServer(event.request, serverPort, path));
175
+ }
176
+ return;
177
+ }
178
+
179
+ // 3. Request from a tracked preview client — route through virtual server.
180
+ // This catches module imports like /@react-refresh, /src/main.tsx, etc.
181
+ // Only intercept same-origin requests; let cross-origin requests
182
+ // (e.g. Google Fonts, external CDNs) pass through to the real server.
183
+ const clientId = event.clientId;
184
+ if (clientId && previewClients.has(clientId)) {
185
+ const host = url.hostname;
186
+ if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === self.location.hostname) {
187
+ const serverPort = previewClients.get(clientId);
188
+ // Strip /__preview__/{port} prefix if the browser resolved a relative URL
189
+ // against the preview page's location (e.g. /__preview__/3001.rsc → /.rsc,
190
+ // /__preview__/3001/foo → /foo)
191
+ let path = url.pathname;
192
+ const ppMatch = path.match(/^\/__preview__\/\d+(.*)?$/);
193
+ if (ppMatch) {
194
+ path = ppMatch[1] || "/";
195
+ if (path[0] !== "/") path = "/" + path;
196
+ }
197
+ path += url.search;
198
+ event.respondWith(proxyToVirtualServer(event.request, serverPort, path, event.request));
199
+ return;
200
+ }
201
+ }
202
+
203
+ // 4. Fallback: check Referer header for /__preview__/ prefix.
204
+ // Handles edge cases where clientId might not be set.
205
+ // Only intercept same-origin requests (not cross-origin like Google Fonts).
206
+ const referer = event.request.referrer;
207
+ if (referer) {
208
+ try {
209
+ const refUrl = new URL(referer);
210
+ const refMatch = refUrl.pathname.match(/^\/__preview__\/(\d+)/);
211
+ if (refMatch) {
212
+ const host = url.hostname;
213
+ if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === self.location.hostname) {
214
+ const serverPort = parseInt(refMatch[1], 10);
215
+ // Strip /__preview__/{port} prefix if present
216
+ let path = url.pathname;
217
+ const ppMatch2 = path.match(/^\/__preview__\/\d+(.*)?$/);
218
+ if (ppMatch2) {
219
+ path = ppMatch2[1] || "/";
220
+ if (path[0] !== "/") path = "/" + path;
221
+ }
222
+ path += url.search;
223
+ // Also register this client for future requests
224
+ if (clientId) {
225
+ previewClients.set(clientId, serverPort);
226
+ }
227
+ event.respondWith(
228
+ proxyToVirtualServer(event.request, serverPort, path, event.request),
229
+ );
230
+ return;
231
+ }
232
+ }
233
+ } catch {
234
+ // Invalid referer URL, ignore
235
+ }
236
+ }
237
+
238
+ // If nothing matched, let the browser handle it normally
239
+ });
240
+
241
+ // ── WebSocket shim for preview iframes ──
242
+ //
243
+ // Injected into HTML responses to override the browser's WebSocket constructor.
244
+ // Routes localhost WebSocket connections through BroadcastChannel "nodepod-ws"
245
+ // to the main thread's request-proxy, which dispatches upgrade events on the
246
+ // virtual HTTP server. Works with any framework/library, not specific to Vite.
247
+
248
+ const WS_SHIM_SCRIPT = `<script>
249
+ (function() {
250
+ if (window.__nodepodWsShim) return;
251
+ window.__nodepodWsShim = true;
252
+ var NativeWS = window.WebSocket;
253
+ var bc = new BroadcastChannel("nodepod-ws");
254
+ var nextId = 0;
255
+ var active = {};
256
+
257
+ // Detect the virtual server port from the page URL.
258
+ // When loaded via /__preview__/{port}/, use that port for WS connections
259
+ // instead of the literal port from the WS URL (which is the host page's port).
260
+ var _previewPort = 0;
261
+ try {
262
+ var _m = location.pathname.match(/^\\/__preview__\\/(\\d+)/);
263
+ if (_m) _previewPort = parseInt(_m[1], 10);
264
+ } catch(e) {}
265
+
266
+ function NodepodWS(url, protocols) {
267
+ var parsed;
268
+ try { parsed = new URL(url, location.href); } catch(e) {
269
+ return new NativeWS(url, protocols);
270
+ }
271
+ // Only intercept localhost connections
272
+ var host = parsed.hostname;
273
+ if (host !== "localhost" && host !== "127.0.0.1" && host !== "0.0.0.0") {
274
+ return new NativeWS(url, protocols);
275
+ }
276
+ var self = this;
277
+ var uid = "ws-iframe-" + (++nextId) + "-" + Math.random().toString(36).slice(2,8);
278
+ // Use the preview port (from /__preview__/{port}/) if available,
279
+ // otherwise fall back to the port from the WebSocket URL.
280
+ var port = _previewPort || parseInt(parsed.port) || (parsed.protocol === "wss:" ? 443 : 80);
281
+ var path = parsed.pathname + parsed.search;
282
+
283
+ self.url = url;
284
+ self.readyState = 0; // CONNECTING
285
+ self.protocol = "";
286
+ self.extensions = "";
287
+ self.bufferedAmount = 0;
288
+ self.binaryType = "blob";
289
+ self.onopen = null;
290
+ self.onclose = null;
291
+ self.onerror = null;
292
+ self.onmessage = null;
293
+ self._uid = uid;
294
+ self._listeners = {};
295
+
296
+ active[uid] = self;
297
+
298
+ bc.postMessage({
299
+ kind: "ws-connect",
300
+ uid: uid,
301
+ port: port,
302
+ path: path,
303
+ protocols: Array.isArray(protocols) ? protocols.join(",") : (protocols || "")
304
+ });
305
+
306
+ // Timeout: if no ws-open within 5s, fire error
307
+ self._connectTimer = setTimeout(function() {
308
+ if (self.readyState === 0) {
309
+ self.readyState = 3;
310
+ var e = new Event("error");
311
+ self.onerror && self.onerror(e);
312
+ _emit(self, "error", e);
313
+ delete active[uid];
314
+ }
315
+ }, 5000);
316
+ }
317
+
318
+ function _emit(ws, evt, arg) {
319
+ var list = ws._listeners[evt];
320
+ if (!list) return;
321
+ for (var i = 0; i < list.length; i++) {
322
+ try { list[i].call(ws, arg); } catch(e) { /* ignore */ }
323
+ }
324
+ }
325
+
326
+ NodepodWS.prototype.addEventListener = function(evt, fn) {
327
+ if (!this._listeners[evt]) this._listeners[evt] = [];
328
+ this._listeners[evt].push(fn);
329
+ };
330
+ NodepodWS.prototype.removeEventListener = function(evt, fn) {
331
+ var list = this._listeners[evt];
332
+ if (!list) return;
333
+ this._listeners[evt] = list.filter(function(f) { return f !== fn; });
334
+ };
335
+ NodepodWS.prototype.dispatchEvent = function(evt) {
336
+ _emit(this, evt.type, evt);
337
+ return true;
338
+ };
339
+ NodepodWS.prototype.send = function(data) {
340
+ if (this.readyState !== 1) throw new Error("WebSocket is not open");
341
+ var type = "text";
342
+ var payload = data;
343
+ if (data instanceof ArrayBuffer) {
344
+ type = "binary";
345
+ payload = Array.from(new Uint8Array(data));
346
+ } else if (data instanceof Uint8Array) {
347
+ type = "binary";
348
+ payload = Array.from(data);
349
+ }
350
+ bc.postMessage({ kind: "ws-send", uid: this._uid, data: payload, type: type });
351
+ };
352
+ NodepodWS.prototype.close = function(code, reason) {
353
+ if (this.readyState >= 2) return;
354
+ this.readyState = 2;
355
+ bc.postMessage({ kind: "ws-close", uid: this._uid, code: code || 1000, reason: reason || "" });
356
+ var self = this;
357
+ setTimeout(function() {
358
+ self.readyState = 3;
359
+ var e = new CloseEvent("close", { code: code || 1000, reason: reason || "", wasClean: true });
360
+ self.onclose && self.onclose(e);
361
+ _emit(self, "close", e);
362
+ delete active[self._uid];
363
+ }, 0);
364
+ };
365
+
366
+ NodepodWS.CONNECTING = 0;
367
+ NodepodWS.OPEN = 1;
368
+ NodepodWS.CLOSING = 2;
369
+ NodepodWS.CLOSED = 3;
370
+ NodepodWS.prototype.CONNECTING = 0;
371
+ NodepodWS.prototype.OPEN = 1;
372
+ NodepodWS.prototype.CLOSING = 2;
373
+ NodepodWS.prototype.CLOSED = 3;
374
+
375
+ bc.onmessage = function(ev) {
376
+ var d = ev.data;
377
+ if (!d || !d.uid) return;
378
+ var ws = active[d.uid];
379
+ if (!ws) return;
380
+
381
+ if (d.kind === "ws-open") {
382
+ clearTimeout(ws._connectTimer);
383
+ ws.readyState = 1;
384
+ var e = new Event("open");
385
+ ws.onopen && ws.onopen(e);
386
+ _emit(ws, "open", e);
387
+ } else if (d.kind === "ws-message") {
388
+ var msgData;
389
+ if (d.type === "binary") {
390
+ msgData = new Uint8Array(d.data).buffer;
391
+ } else {
392
+ msgData = d.data;
393
+ }
394
+ var me = new MessageEvent("message", { data: msgData });
395
+ ws.onmessage && ws.onmessage(me);
396
+ _emit(ws, "message", me);
397
+ } else if (d.kind === "ws-closed") {
398
+ ws.readyState = 3;
399
+ clearTimeout(ws._connectTimer);
400
+ var ce = new CloseEvent("close", { code: d.code || 1000, reason: "", wasClean: true });
401
+ ws.onclose && ws.onclose(ce);
402
+ _emit(ws, "close", ce);
403
+ delete active[d.uid];
404
+ } else if (d.kind === "ws-error") {
405
+ ws.readyState = 3;
406
+ clearTimeout(ws._connectTimer);
407
+ var ee = new Event("error");
408
+ ws.onerror && ws.onerror(ee);
409
+ _emit(ws, "error", ee);
410
+ delete active[d.uid];
411
+ }
412
+ };
413
+
414
+ window.WebSocket = NodepodWS;
415
+ })();
416
+ </script>`;
417
+
418
+ // Small "nodepod" badge in the bottom-right corner of preview iframes.
419
+ const WATERMARK_SCRIPT = `<script>
420
+ (function() {
421
+ if (window.__nodepodWatermark) return;
422
+ window.__nodepodWatermark = true;
423
+ document.addEventListener("DOMContentLoaded", function() {
424
+ var a = document.createElement("a");
425
+ a.href = "https://github.com/ScelarOrg/Nodepod";
426
+ a.target = "_blank";
427
+ a.rel = "noopener noreferrer";
428
+ a.textContent = "nodepod";
429
+ a.style.cssText = "position:fixed;bottom:6px;right:8px;z-index:2147483647;"
430
+ + "font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:11px;"
431
+ + "color:rgba(255,255,255,0.45);background:rgba(0,0,0,0.25);padding:2px 6px;"
432
+ + "border-radius:4px;text-decoration:none;pointer-events:auto;transition:color .15s;";
433
+ a.onmouseenter = function() { a.style.color = "rgba(255,255,255,0.85)"; };
434
+ a.onmouseleave = function() { a.style.color = "rgba(255,255,255,0.45)"; };
435
+ document.body.appendChild(a);
436
+ });
437
+ })();
438
+ </script>`;
439
+
440
+ // ── Error page generator ──
441
+
442
+ function errorPage(status, title, message) {
443
+ const html = `<!DOCTYPE html>
444
+ <html lang="en">
445
+ <head>
446
+ <meta charset="utf-8">
447
+ <meta name="viewport" content="width=device-width, initial-scale=1">
448
+ <title>${status} - ${title}</title>
449
+ <style>
450
+ * { margin: 0; padding: 0; box-sizing: border-box; }
451
+ body {
452
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
453
+ background: #0a0a0a; color: #e0e0e0;
454
+ display: flex; align-items: center; justify-content: center;
455
+ min-height: 100vh; padding: 2rem;
456
+ }
457
+ .container { max-width: 480px; text-align: center; }
458
+ .status { font-size: 5rem; font-weight: 700; color: #555; line-height: 1; }
459
+ .title { font-size: 1.25rem; margin-top: 0.75rem; color: #ccc; }
460
+ .message { font-size: 0.875rem; margin-top: 1rem; color: #888; line-height: 1.5; }
461
+ .hint { font-size: 0.8rem; margin-top: 1.5rem; color: #555; }
462
+ </style>
463
+ </head>
464
+ <body>
465
+ <div class="container">
466
+ <div class="status">${status}</div>
467
+ <div class="title">${title}</div>
468
+ <div class="message">${message}</div>
469
+ <div class="hint">Powered by Nodepod</div>
470
+ </div>
471
+ </body>
472
+ </html>`;
473
+ return new Response(html, {
474
+ status,
475
+ statusText: title,
476
+ headers: {
477
+ "content-type": "text/html; charset=utf-8",
478
+ "Cross-Origin-Resource-Policy": "cross-origin",
479
+ "Cross-Origin-Embedder-Policy": "credentialless",
480
+ "Cross-Origin-Opener-Policy": "same-origin",
481
+ },
482
+ });
483
+ }
484
+
485
+ // ── Virtual server proxy ──
486
+
487
+ async function proxyToVirtualServer(request, serverPort, path, originalRequest) {
488
+ if (!port) {
489
+ const clients = await self.clients.matchAll();
490
+ for (const client of clients) {
491
+ client.postMessage({ type: "sw-needs-init" });
492
+ }
493
+ await new Promise((r) => setTimeout(r, 200));
494
+ if (!port) {
495
+ return errorPage(503, "Service Unavailable", "The Nodepod service worker is still initializing. Please refresh the page.");
496
+ }
497
+ }
498
+
499
+ // Clone the original request before consuming the body, so we can use it
500
+ // for the 404 fallback fetch later if needed.
501
+ const fallbackRequest = originalRequest ? originalRequest.clone() : null;
502
+
503
+ const headers = {};
504
+ request.headers.forEach((v, k) => {
505
+ headers[k] = v;
506
+ });
507
+ headers["host"] = `localhost:${serverPort}`;
508
+
509
+ let body = undefined;
510
+ if (request.method !== "GET" && request.method !== "HEAD") {
511
+ try {
512
+ body = await request.arrayBuffer();
513
+ } catch {
514
+ // body not available
515
+ }
516
+ }
517
+
518
+ const id = nextId++;
519
+ const promise = new Promise((resolve, reject) => {
520
+ pending.set(id, { resolve, reject });
521
+ setTimeout(() => {
522
+ if (pending.has(id)) {
523
+ pending.delete(id);
524
+ reject(new Error("Request timeout: " + path));
525
+ }
526
+ }, 30000);
527
+ });
528
+
529
+ port.postMessage({
530
+ type: "request",
531
+ id,
532
+ data: {
533
+ port: serverPort,
534
+ method: request.method,
535
+ url: path,
536
+ headers,
537
+ body,
538
+ // Pass the full original URL so the main thread can do a fallback
539
+ // network fetch if the virtual server returns 404. This handles
540
+ // cross-origin resources (fonts, CDN assets) that the preview app
541
+ // references but the virtual server doesn't serve.
542
+ originalUrl: request.url,
543
+ },
544
+ });
545
+
546
+ try {
547
+ const data = await promise;
548
+ let responseBody = null;
549
+ if (data.bodyBase64) {
550
+ const binary = atob(data.bodyBase64);
551
+ const bytes = new Uint8Array(binary.length);
552
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
553
+ responseBody = bytes;
554
+ }
555
+ const respHeaders = Object.assign({}, data.headers || {});
556
+
557
+ // Fix MIME type: SPA fallback middleware may serve index.html (text/html)
558
+ // for non-HTML paths. Correct the Content-Type based on file extension.
559
+ const overrideMime = inferMimeType(path, respHeaders);
560
+ if (overrideMime) {
561
+ // Replace Content-Type regardless of casing in original headers
562
+ for (const k of Object.keys(respHeaders)) {
563
+ if (k.toLowerCase() === "content-type") delete respHeaders[k];
564
+ }
565
+ respHeaders["content-type"] = overrideMime;
566
+ }
567
+
568
+ // Inject WebSocket shim + preview script into HTML responses so that
569
+ // browser-side WebSocket connections are routed through nodepod, and
570
+ // user-provided preview scripts run before any page content.
571
+ let finalBody = responseBody;
572
+ const ct = respHeaders["content-type"] || respHeaders["Content-Type"] || "";
573
+ if (ct.includes("text/html") && responseBody) {
574
+ let injection = WS_SHIM_SCRIPT;
575
+ if (previewScript) {
576
+ injection += `<script>${previewScript}<` + `/script>`;
577
+ }
578
+ if (watermarkEnabled) {
579
+ injection += WATERMARK_SCRIPT;
580
+ }
581
+ const html = new TextDecoder().decode(responseBody);
582
+ // Inject before <head> or at the start of the document
583
+ const headIdx = html.indexOf("<head");
584
+ if (headIdx >= 0) {
585
+ const closeAngle = html.indexOf(">", headIdx);
586
+ if (closeAngle >= 0) {
587
+ const injected = html.slice(0, closeAngle + 1) + injection + html.slice(closeAngle + 1);
588
+ finalBody = new TextEncoder().encode(injected);
589
+ }
590
+ } else {
591
+ // No <head> tag — prepend the shim
592
+ finalBody = new TextEncoder().encode(injection + html);
593
+ }
594
+ // Update content-length if present
595
+ for (const k of Object.keys(respHeaders)) {
596
+ if (k.toLowerCase() === "content-length") {
597
+ respHeaders[k] = String(finalBody.byteLength);
598
+ }
599
+ }
600
+ }
601
+
602
+ // Ensure COEP compatibility: the parent page sets
603
+ // Cross-Origin-Embedder-Policy: credentialless, so all sub-resources
604
+ // (including iframe content served by this SW) need CORP headers.
605
+ // Additionally, iframe HTML documents need their own COEP/COOP headers
606
+ // so that subresources loaded by the iframe are also allowed.
607
+ if (!respHeaders["cross-origin-resource-policy"] && !respHeaders["Cross-Origin-Resource-Policy"]) {
608
+ respHeaders["Cross-Origin-Resource-Policy"] = "cross-origin";
609
+ }
610
+ if (!respHeaders["cross-origin-embedder-policy"] && !respHeaders["Cross-Origin-Embedder-Policy"]) {
611
+ respHeaders["Cross-Origin-Embedder-Policy"] = "credentialless";
612
+ }
613
+ if (!respHeaders["cross-origin-opener-policy"] && !respHeaders["Cross-Origin-Opener-Policy"]) {
614
+ respHeaders["Cross-Origin-Opener-Policy"] = "same-origin";
615
+ }
616
+
617
+ // If the virtual server returned 404 and we have the original request,
618
+ // fall back to a real network fetch. This handles cases where the preview
619
+ // app generates relative URLs for external resources (e.g. fonts, CDN assets)
620
+ // that the virtual server doesn't serve.
621
+ if ((data.statusCode === 404) && fallbackRequest) {
622
+ try {
623
+ return await fetch(fallbackRequest);
624
+ } catch (fetchErr) {
625
+ // Fall through to return the original 404
626
+ }
627
+ }
628
+
629
+ return new Response(finalBody, {
630
+ status: data.statusCode || 200,
631
+ statusText: data.statusMessage || "OK",
632
+ headers: respHeaders,
633
+ });
634
+ } catch (err) {
635
+ const msg = err.message || "Proxy error";
636
+ // If the error is a timeout, it likely means no server is listening
637
+ if (msg.includes("timeout")) {
638
+ return errorPage(504, "Gateway Timeout", "No server responded on port " + serverPort + ". Make sure your dev server is running.");
639
+ }
640
+ return errorPage(502, "Bad Gateway", msg);
641
+ }
642
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
4
 
5
- const index = require('./index-DsMGS-xc.cjs');
5
+ const index = require('./index-cnitc68U.cjs');
6
6
 
7
7
  function expandVariables(raw, env, lastExit) {
8
8
  let result = "";
@@ -6091,7 +6091,7 @@ function formatWarn(msg, pm) {
6091
6091
  }
6092
6092
  }
6093
6093
  async function installPackages(args, ctx, pm = "npm") {
6094
- const { DependencyInstaller } = await Promise.resolve().then(() => require('./index-DsMGS-xc.cjs')).then(n => n.installer);
6094
+ const { DependencyInstaller } = await Promise.resolve().then(() => require('./index-cnitc68U.cjs')).then(n => n.installer);
6095
6095
  const installer = new DependencyInstaller(_vol, { cwd: ctx.cwd });
6096
6096
  let out = "";
6097
6097
  const write = _stdoutSink ?? ((_s) => {
@@ -6188,7 +6188,7 @@ async function uninstallPackages(args, ctx, pm = "npm") {
6188
6188
  return { stdout: out, stderr: "", exitCode: 0 };
6189
6189
  }
6190
6190
  async function listPackages(ctx, pm = "npm") {
6191
- const { DependencyInstaller } = await Promise.resolve().then(() => require('./index-DsMGS-xc.cjs')).then(n => n.installer);
6191
+ const { DependencyInstaller } = await Promise.resolve().then(() => require('./index-cnitc68U.cjs')).then(n => n.installer);
6192
6192
  const installer = new DependencyInstaller(_vol, { cwd: ctx.cwd });
6193
6193
  const pkgs = installer.listInstalled();
6194
6194
  const entries = Object.entries(pkgs);
@@ -6325,7 +6325,7 @@ async function npmInfo(args, ctx) {
6325
6325
  }
6326
6326
  }
6327
6327
  try {
6328
- const { RegistryClient } = await Promise.resolve().then(() => require('./index-DsMGS-xc.cjs')).then(n => n.registryClient);
6328
+ const { RegistryClient } = await Promise.resolve().then(() => require('./index-cnitc68U.cjs')).then(n => n.registryClient);
6329
6329
  const client = new RegistryClient();
6330
6330
  const meta = await client.fetchManifest(name);
6331
6331
  const latest = meta["dist-tags"]?.latest;
@@ -7431,4 +7431,4 @@ exports.setSyncChannel = setSyncChannel;
7431
7431
  exports.shellExec = shellExec;
7432
7432
  exports.spawn = spawn;
7433
7433
  exports.spawnSync = spawnSync;
7434
- //# sourceMappingURL=child_process-Cj8vOcuc.cjs.map
7434
+ //# sourceMappingURL=child_process-B38qoN6R.cjs.map