@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 +642 -0
- package/dist/{child_process-Cj8vOcuc.cjs → child_process-B38qoN6R.cjs} +5 -5
- package/dist/{child_process-Cj8vOcuc.cjs.map → child_process-B38qoN6R.cjs.map} +1 -1
- package/dist/{child_process-BJOMsZje.js → child_process-Dopvyd-E.js} +4 -4
- package/dist/{child_process-BJOMsZje.js.map → child_process-Dopvyd-E.js.map} +1 -1
- package/dist/{index-Cb1Cgdnd.js → index--Qr8LVpQ.js} +4 -4
- package/dist/index--Qr8LVpQ.js.map +1 -0
- package/dist/{index-DsMGS-xc.cjs → index-cnitc68U.cjs} +7 -7
- package/dist/index-cnitc68U.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/index-Cb1Cgdnd.js.map +0 -1
- package/dist/index-DsMGS-xc.cjs.map +0 -1
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
7434
|
+
//# sourceMappingURL=child_process-B38qoN6R.cjs.map
|