UNPKG

node-network-devtools

Version:

Inspecting Node.js's Network with Chrome DevTools

631 lines (630 loc) 18.2 kB
var H = (t) => { throw TypeError(t); }; var O = (t, e, s) => e.has(t) || H("Cannot " + s); var D = (t, e, s) => (O(t, e, "read from private field"), s ? s.call(t) : e.get(t)), C = (t, e, s) => e.has(t) ? H("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, s), v = (t, e, s, r) => (O(t, e, "write to private field"), r ? r.call(t, s) : e.set(t, s), s); import { Server as J, WebSocket as K } from "ws"; import Y, { apps as Q } from "open"; import { l as y, I as X, c as U, P as z, a as Z, _ as ee, R as E, p as te, f as j, s as se, g as L, S as re } from "./common-D7uiq4bu.mjs"; import { fileURLToPath as ne, pathToFileURL as P } from "url"; import _ from "iconv-lite"; import N from "node:zlib"; import S from "fs"; import M from "path"; const oe = (t, e) => { try { return JSON.parse(t); } catch { return e; } }; class ae { constructor() { this.timestamp = 0, this.startTime = Date.now(), this.listeners = []; } getTimestamp() { return this.updateTimestamp(), this.timestamp; } updateTimestamp() { this.timestamp = (Date.now() - this.startTime) / 1e3; } on(e) { this.listeners.push(e); } } class ie extends ae { constructor(e) { super(), this.browser = null; const { port: s, autoOpenDevtool: r = !0, onConnect: a, onClose: i } = e; this.port = s, this.server = new J({ port: s }); const { server: u } = this; u.on("listening", () => { y(`devtool server is listening on port ${s}`), r && this.open(); }), this.socket = new Promise((p) => { u.on("connection", (n) => { a?.(), this.socket.then((o) => { o[0] = n; }), y("devtool connected"), n.on("message", (o) => { const c = JSON.parse(o.toString()); this.listeners.forEach((d) => d(null, c)); }), n.on("close", () => { y("devtool closed"), i?.(); }), n.on("error", (o) => { this.listeners.forEach((c) => c(o)); }), p([n]); }); }); } async open() { const e = `devtools://devtools/bundled/inspector.html?ws=127.0.0.1:${this.port}`; try { if (X) { y(`In dev mode, open chrome devtool manually: ${e}`); return; } const s = await Y(e, { app: { name: Q.chrome, arguments: [ process.platform !== "darwin" ? `--remote-debugging-port=${U}` : "" ] }, wait: !0 }); e: if (process.platform !== "darwin") { const r = await new Promise( (n) => { let o = 0, c = setInterval(async () => { o > 5 && (clearInterval(c), n([])); try { o++, n((await fetch(`http://127.0.0.1:${U}/json`)).json()), clearInterval(c); } catch { } }, 500); } ), [a] = r; if (!a) break e; const { id: i, webSocketDebuggerUrl: u } = a, p = new K(u); p.on("open", () => { const n = { id: i, method: "Page.navigate", params: { url: e } }; p.send(JSON.stringify(n)), p.close(); }); } y("opened in chrome or click here to open chrome devtool: ", e), this.browser = s; return; } catch { console.warn( "Open devtools failed, but don't worry, you can open it in browser(Chrome or Edge) manually: " + e ); } } close() { this.server.close(), this.browser && this.browser.kill(); } async send(e) { const [s] = await this.socket; return s.send(JSON.stringify(e)); } } var I, w; class ce { constructor(e) { C(this, I); C(this, w); this.listeners = {}, v(this, I, []), this.options = e; const { serverPort: s, autoOpenDevtool: r } = e; this.devtool = new ie({ port: s, autoOpenDevtool: r, onConnect: () => { this.listeners.onConnect?.forEach( (i) => i({ data: null, id: "onConnect" }) ); } }), this.devtool.on((a, i) => { if (a) { y(a); return; } const u = this.listeners[i.method]; u && u.forEach((p) => { p({ data: i.params, id: i.id }); }); }), this.server = this.initServer(); } loadPlugins(e) { v(this, I, e), v(this, w, /* @__PURE__ */ new Map()), e.forEach((s) => { const r = s({ devtool: this.devtool, core: this, plugins: D(this, I) }); D(this, w).set(s.id, r); }); } usePlugin(e) { return D(this, w) ? D(this, w).get(e) : null; } on(e, s) { return this.listeners[e] || (this.listeners[e] = /* @__PURE__ */ new Set()), this.listeners[e].add(s), () => { this.listeners[e].delete(s); }; } close() { this.server.close(), this.devtool.close(); } initServer() { const e = new J({ port: this.options.port || z }); return e.on("connection", (s) => { s.on("message", (r) => { const i = JSON.parse(r.toString()); switch (i.type) { default: { const u = this.listeners[i.type]; if (!u) break; u.forEach((p) => { p({ data: i.data }); }); } break; } }); }), e.on("listening", () => { process.send && process.send(Z); }), e; } } I = new WeakMap(), w = new WeakMap(); let k = null; const ue = (t, e) => { k = { devtool: e, core: t }; }, de = () => k = null, q = (t, e) => Object.assign((a) => { ue(a.core, a.devtool); const i = e(a); return de(), i; }, { id: t }), m = (t, e) => { if (!k) return; const s = k, { core: r } = s; return r.on(t, e); }, pe = (t) => { if (!k) return; const { core: e } = k; return e.on("onConnect", t); }, le = q("debugger", ({ devtool: t, core: e }) => { const s = e.usePlugin("network"); m("Debugger.getScriptSource", ({ id: a, data: i }) => { if (!a) return; const { scriptId: u } = i, p = s.resourceService.getScriptSource(u); t.send({ id: a, method: "Debugger.getScriptSourceResponse", result: { scriptSource: p } }); }); const r = s.resourceService.getLocalScriptList(); pe(() => { r.forEach((a) => { t.send({ method: "Debugger.scriptParsed", params: a }); }); }); }); class R { constructor(e) { this.headers = { ...e || {} }, Object.keys(this.headers).forEach((s) => { this.headers[s] = String(this.headers[s]); }); } getHeader(e) { const s = e.toLowerCase(), r = Object.keys(this.headers); for (const a of r) if (a.toLowerCase() === s) return this.headers[a]; } getData() { return this.headers; } valueOf() { return this.headers; } } class he { constructor(e) { this.req = e; } decodeBody() { const { req: e } = this, r = new R(e.responseHeaders).getHeader("content-type") || "text/plain; charset=utf-8", a = r.match(/charset=([^;]+)/), i = a ? a[1] : "utf-8", u = !/text|json|xml/.test(r); return { body: (() => { if (!(e.responseData === void 0 || e.responseData === null)) return u ? e.responseData.toString("base64") : Buffer.isBuffer(e.responseData) ? _.decode(e.responseData, i) : typeof e.responseData == "object" && "type" in e.responseData && e.responseData.type === "Buffer" && "data" in e.responseData && Array.isArray(e.responseData.data) ? _.decode(Buffer.from(e.responseData.data), i) : e.responseData; })(), base64Encoded: u }; } } function me(t) { switch (t.split(".").pop()?.toLowerCase()) { case "js": case "mjs": case "cjs": return "JavaScript"; case "wasm": return "WebAssembly"; default: return "Unknown"; } } class ge { constructor() { this.urlToScriptId = /* @__PURE__ */ new Map(), this.scriptIdToUrl = /* @__PURE__ */ new Map(); } addMapping(e, s) { this.urlToScriptId.set(e, s), this.scriptIdToUrl.set(s, e); } getUrlByScriptId(e) { return this.scriptIdToUrl.get(e); } getScriptIdByUrl(e) { return this.urlToScriptId.get(e); } } class fe { constructor() { this.scriptMap = new ge(), this.scriptIdCounter = 0; } getScriptIdByUrl(e) { return this.scriptMap.getScriptIdByUrl(e); } getUrlByScriptId(e) { return this.scriptMap.getUrlByScriptId(e); } getScriptSource(e) { const s = this.scriptMap.getUrlByScriptId(e); if (!s) return console.error(`No file path found for script ID: ${e}`), null; const r = ne(s); try { return S.readFileSync(r, "utf-8"); } catch (a) { return console.error("Error reading file:", a), null; } } /** * @description Read the last lines of the file * @param filePath * @param stat * @param totalLines * @returns string */ readLastLine(e, s, r = 1) { const a = s.size, i = Math.min(1024, a); let u = a - i, p = Buffer.alloc(i), n = []; const o = S.openSync(e, "r"); for (; n.length < r && u >= 0; ) S.readSync(o, p, 0, i, u), n = p.toString("utf8").split(` `).concat(n), u -= i, u < 0 && (u = 0, p = Buffer.alloc(a - u)); return S.closeSync(o), n.slice(-r).join(` `); } traverseDirToMap(e, s = ["node_modules"]) { const r = [], a = [e]; let i = this.scriptIdCounter; for (; a.length > 0; ) { const u = a.pop(), p = S.readdirSync(u); for (const n of p) { if (s.includes(n)) continue; const o = M.join(u, n), c = S.statSync(o); if (c.isDirectory()) a.push(o); else { const d = M.resolve(o), h = P(d), l = `${++i}`; let g = ""; if (/\.(js|ts)$/.test(d)) { const b = this.readLastLine(o, c, 2).match(/sourceMappingURL=(.+)$/m)?.[1] ?? ""; g = b ? b.startsWith("data:") ? ( // inline sourcemap b ) : ( // file path P(M.join(u, b)).href ) : ""; } const f = h.href, V = me(f); r.push({ url: f, scriptLanguage: V, embedderName: h.href, scriptId: l, sourceMapURL: g, hasSourceURL: !!g }), this.scriptMap.addMapping(f, l); } } } return this.scriptIdCounter += r.length, r; } getLocalScriptList() { const e = this.traverseDirToMap(process.cwd()), s = this.traverseDirToMap(ee); return [...e, ...s]; } } const $ = "517.528", F = "517.529", we = (t) => t.split(";")[0] || "text/plain", Se = q("network", ({ devtool: t, core: e }) => { const s = {}, r = new fe(), a = (n) => s[n], i = (n) => { s[n.id] = n; }, u = (n) => { s[n.id] = n, n.requestEndTime = n.requestEndTime || Date.now(), t.updateTimestamp(); const c = new R(n.responseHeaders).getHeader("content-type") || "text/plain; charset=utf-8", d = /text\/event-stream/.test(c) ? "EventSource" : /image/.test(c) ? "Image" : /javascript/.test(c) ? "Script" : /css/.test(c) ? "Stylesheet" : /html/.test(c) ? "Document" : "Other"; t.send({ method: "Network.responseReceived", params: { requestId: n.id, frameId: $, loaderId: F, timestamp: t.timestamp, type: d, response: { url: n.url, status: n.responseStatusCode, statusText: n.responseStatusCode === 200 ? "OK" : "", headers: n.responseHeaders, connectionReused: !1, encodedDataLength: n.responseInfo.encodedDataLength, charset: "utf-8", mimeType: we(c) } } }), t.updateTimestamp(), t.send({ method: "Network.dataReceived", params: { requestId: n.id, timestamp: t.timestamp, dataLength: n.responseInfo.dataLength, encodedDataLength: n.responseInfo.encodedDataLength } }), t.updateTimestamp(), t.send({ method: "Network.loadingFinished", params: { requestId: n.id, timestamp: t.timestamp, encodedDataLength: n.responseInfo.encodedDataLength } }); }, p = (n, o) => { const c = [N.gunzip, N.inflate, N.brotliDecompress]; let d = 0; const h = () => { if (d >= c.length) { o(n); return; } const l = c[d]; d += 1, l(n, (g, f) => { g ? h() : o(f); }); }; h(); }; return m("Network.getResponseBody", ({ data: n, id: o }) => { const c = a(n.requestId); if (!o || !c) { console.error("request is not found"); return; } const d = new he(c).decodeBody(); t.send({ id: o, result: d }); }), m("initRequest", ({ data: n }) => { const o = new E(n); o.initiator && o.initiator.stack.callFrames.forEach((c) => { const d = P(c.url), h = r.getScriptIdByUrl(d.href) ?? r.getScriptIdByUrl(c.url); h && (c.scriptId = h); }), s[o.id] = o; }), m("registerRequest", ({ data: n }) => { const o = new E(n); s[o.id] = o, o.initiator && o.initiator.stack.callFrames.forEach((d) => { const h = P(d.url), l = r.getScriptIdByUrl(h.href) ?? r.getScriptIdByUrl(d.url); l && (d.scriptId = l); }), t.updateTimestamp(); const c = new R(o.requestHeaders); return c.getHeader("content-type"), t.send({ method: "Network.requestWillBeSent", params: { requestId: o.id, frameId: $, loaderId: F, request: { url: o.url, method: o.method, headers: c.getData(), initialPriority: "High", mixedContentType: "none", ...o.requestData ? { postData: o.requestData.toString() } : {} }, timestamp: t.timestamp, wallTime: o.requestStartTime, initiator: o.initiator, type: o.isWebSocket() ? "WebSocket" : "Fetch" } }); }), m("endRequest", ({ data: n }) => { const o = new E(n); u(o); }), m("responseData", ({ data: n }) => { const { id: o, rawData: c, statusCode: d, headers: h } = n, l = a(o), g = Buffer.from(c); l && (l.responseInfo.encodedDataLength = g.length, p(g, (f) => { l.responseData = f, l.responseInfo.dataLength = f.length, l.responseStatusCode = d, l.responseHeaders = new R(h).getData(), i(l), u(l); })); }), m("eventSourceMessage", ({ data: n }) => { const { requestId: o, eventName: c, eventId: d, data: h } = n; a(o) && (t.updateTimestamp(), t.send({ method: "Network.eventSourceMessageReceived", params: { requestId: o, timestamp: t.timestamp, eventName: c, eventId: d, data: h } })); }), { getRequest: a, resourceService: r }; }), ye = q("websocket", ({ devtool: t, core: e }) => { const s = e.usePlugin("network"); m( "Network.webSocketCreated", async ({ data: { response: r, requestId: a } }) => { if (!a) return; const i = s.getRequest(a); if (!i) return; const u = te(r.rawHeaders); await t.send({ method: "Network.webSocketCreated", params: { url: i.url, initiator: i.initiator, requestId: i.id } }), await t.send({ method: "Network.webSocketWillSendHandshakeRequest", params: { wallTime: Date.now(), timestamp: t.getTimestamp(), requestId: i.id, request: { headers: i.requestHeaders } } }), await t.send({ method: "Network.webSocketHandshakeResponseReceived", params: { requestId: i.id, response: { headers: u, headersText: j( `HTTP/${r.httpVersion} ${r.statusCode} ${r.statusMessage}\r `, u ), status: r.statusCode, statusText: r.statusMessage, requestHeadersText: j( `${i.method} ${i.url} HTTP/${r.httpVersion}\r `, i.requestHeaders ), requestHeaders: se(i.requestHeaders) }, timestamp: t.getTimestamp() } }); } ), m("Network.webSocketFrameSent", async ({ data: r }) => { r.requestId && await t.send({ method: "Network.webSocketFrameSent", params: { requestId: r.requestId, response: r.response, // Network中数据时间戳 timestamp: L() } }); }), m("Network.webSocketFrameReceived", async ({ data: r }) => { r.requestId && await t.send({ method: "Network.webSocketFrameReceived", params: { requestId: r.requestId, response: r.response, timestamp: L() } }); }), m("Network.webSocketClosed", async ({ data: r }) => { r.requestId && await t.send({ method: "Network.webSocketClosed", params: { requestId: r.requestId, timestamp: L() } }); }); }), Ie = q("health", ({ devtool: t }) => { const e = () => { process.exit(0); }; let s = setTimeout(e, 1e4); m("healthcheck", () => { clearTimeout(s), s = setTimeout(e, 1e4); }); }), ke = (t) => { t.loadPlugins([Ie, Se, le, ye]); }, B = oe(process.env.NETWORK_OPTIONS || "{}", {}), A = () => { const t = new ce({ serverPort: B.serverPort || re, port: B.port || z, autoOpenDevtool: B.autoOpenDevtool }); return ke(t), t; }; let W = A(), x = 0; const De = 5, Te = 30 * 1e3, G = () => { setTimeout( () => { if (x++, x >= De) { console.error("Restart limit reached"), T(); return; } W.close(), W = A(); }, 10 + Math.random() * 100 ); }; setInterval(() => { x = 0; }, Te); const T = () => { process.exit(0); }; process.on("exit", T); process.on("SIGINT", T); process.on("SIGTERM", T); process.on("beforeExit", T); process.on("uncaughtException", (t) => { console.error("uncaughtException: ", t), G(); }); process.on("unhandledRejection", (t) => { console.error("unhandledRejection: ", t), G(); });