UNPKG

node-network-devtools

Version:

Inspecting Node.js's Network with Chrome DevTools

621 lines (620 loc) 17.9 kB
var x = (t) => { throw TypeError(t); }; var H = (t, e, s) => e.has(t) || x("Cannot " + s); var D = (t, e, s) => (H(t, e, "read from private field"), s ? s.call(t) : e.get(t)), C = (t, e, s) => e.has(t) ? x("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, s), v = (t, e, s, r) => (H(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 L, p as te, f as j, s as se, g as E, S as re } from "./common-BUk9ORDJ.mjs"; import { fileURLToPath as ne, pathToFileURL as P } from "url"; import _ from "iconv-lite"; import N from "node:zlib"; import S from "fs"; import B from "path"; const oe = (t, e) => { try { return JSON.parse(t); } catch { return e; } }; class ie { 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 ae extends ie { constructor(e) { super(), this.browser = null; const { port: s, autoOpenDevtool: r = !0, onConnect: i, onClose: a } = e; this.port = s, this.server = new J({ port: s }); const { server: c } = this; c.on("listening", () => { y(`devtool server is listening on port ${s}`), r && this.open(); }), this.socket = new Promise((p) => { c.on("connection", (n) => { i?.(), this.socket.then((o) => { o[0] = n; }), y("devtool connected"), n.on("message", (o) => { const u = JSON.parse(o.toString()); this.listeners.forEach((d) => d(null, u)); }), n.on("close", () => { y("devtool closed"), a?.(); }), n.on("error", (o) => { this.listeners.forEach((u) => u(o)); }), p([n]); }); }); } async open() { const e = `devtools://devtools/bundled/inspector.html?ws=localhost:${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, u = setInterval(async () => { o > 5 && (clearInterval(u), n([])); try { o++, n((await fetch(`http://localhost:${U}/json`)).json()), clearInterval(u); } catch { } }, 500); } ), [i] = r; if (!i) break e; const { id: a, webSocketDebuggerUrl: c } = i, p = new K(c); p.on("open", () => { const n = { id: a, 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 ae({ port: s, autoOpenDevtool: r, onConnect: () => { this.listeners.onConnect?.forEach( (a) => a({ data: null, id: "onConnect" }) ); } }), this.devtool.on((i, a) => { if (i) { y(i); return; } const c = this.listeners[a.method]; c && c.forEach((p) => { p({ data: a.params, id: a.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 a = JSON.parse(r.toString()); switch (a.type) { default: { const c = this.listeners[a.type]; if (!c) break; c.forEach((p) => { p({ data: a.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 }; }, pe = () => k = null, q = (t, e) => Object.assign((i) => { ue(i.core, i.devtool); const a = e(i); return pe(), a; }, { id: t }), g = (t, e) => { if (!k) return; const s = k, { core: r } = s; return r.on(t, e); }, de = (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"); g("Debugger.getScriptSource", ({ id: i, data: a }) => { if (!i) return; const { scriptId: c } = a, p = s.resourceService.getScriptSource(c); t.send({ id: i, method: "Debugger.getScriptSourceResponse", result: { scriptSource: p } }); }); const r = s.resourceService.getLocalScriptList(); de(() => { r.forEach((i) => { t.send({ method: "Debugger.scriptParsed", params: i }); }); }); }); 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 i of r) if (i.toLowerCase() === s) return this.headers[i]; } 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", i = r.match(/charset=([^;]+)/), a = i ? i[1] : "utf-8", c = !/text|json|xml/.test(r); return { body: (() => { if (!(e.responseData === void 0 || e.responseData === null)) return c ? e.responseData.toString("base64") : Buffer.isBuffer(e.responseData) ? _.decode(e.responseData, a) : 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), a) : e.responseData; })(), base64Encoded: c }; } } 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 (i) { return console.error("Error reading file:", i), null; } } /** * @description Read the last lines of the file * @param filePath * @param stat * @param totalLines * @returns string */ readLastLine(e, s, r = 1) { const i = s.size, a = Math.min(1024, i); let c = i - a, p = Buffer.alloc(a), n = []; const o = S.openSync(e, "r"); for (; n.length < r && c >= 0; ) S.readSync(o, p, 0, a, c), n = p.toString("utf8").split(` `).concat(n), c -= a, c < 0 && (c = 0, p = Buffer.alloc(i - c)); return S.closeSync(o), n.slice(-r).join(` `); } traverseDirToMap(e, s = ["node_modules"]) { const r = [], i = [e]; let a = this.scriptIdCounter; for (; i.length > 0; ) { const c = i.pop(), p = S.readdirSync(c); for (const n of p) { if (s.includes(n)) continue; const o = B.join(c, n), u = S.statSync(o); if (u.isDirectory()) i.push(o); else { const d = B.resolve(o), h = P(d), l = `${++a}`; let m = ""; if (/\.(js|ts)$/.test(d)) { const b = this.readLastLine(o, u, 2).match(/sourceMappingURL=(.+)$/m)?.[1] ?? ""; m = b ? b.startsWith("data:") ? ( // inline sourcemap b ) : ( // file path P(B.join(c, b)).href ) : ""; } const f = h.href, V = me(f); r.push({ url: f, scriptLanguage: V, embedderName: h.href, scriptId: l, sourceMapURL: m, hasSourceURL: !!m }), 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(), i = (n) => s[n], a = (n) => { s[n.id] = n; }, c = (n) => { n.requestEndTime = n.requestEndTime || Date.now(), t.updateTimestamp(); const u = new R(n.responseHeaders).getHeader("content-type") || "text/plain; charset=utf-8", d = /image/.test(u) ? "Image" : /javascript/.test(u) ? "Script" : /css/.test(u) ? "Stylesheet" : /html/.test(u) ? "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(u) } } }), 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 u = [N.gunzip, N.inflate, N.brotliDecompress]; let d = 0; const h = () => { if (d >= u.length) { o(n); return; } const l = u[d]; d += 1, l(n, (m, f) => { m ? h() : o(f); }); }; h(); }; return g("Network.getResponseBody", ({ data: n, id: o }) => { const u = i(n.requestId); if (!o || !u) { console.error("request is not found"); return; } const d = new he(u).decodeBody(); t.send({ id: o, result: d }); }), g("initRequest", ({ data: n }) => { const o = new L(n); o.initiator && o.initiator.stack.callFrames.forEach((u) => { const d = P(u.url), h = r.getScriptIdByUrl(d.href) ?? r.getScriptIdByUrl(u.url); h && (u.scriptId = h); }), s[o.id] = o; }), g("registerRequest", ({ data: n }) => { const o = new L(n); s[o.id] = o, o.initiator && o.initiator.stack.callFrames.forEach((h) => { const l = P(h.url), m = r.getScriptIdByUrl(l.href) ?? r.getScriptIdByUrl(h.url); m && (h.scriptId = m); }), t.updateTimestamp(); const u = new R(o.requestHeaders), d = u.getHeader("content-type"); return t.send({ method: "Network.requestWillBeSent", params: { requestId: o.id, frameId: $, loaderId: F, request: { url: o.url, method: o.method, headers: u.getData(), initialPriority: "High", mixedContentType: "none", ...o.requestData ? { postData: d?.includes("application/json") ? JSON.stringify(o.requestData) : o.requestData } : {} }, timestamp: t.timestamp, wallTime: o.requestStartTime, initiator: o.initiator, type: o.isWebSocket() ? "WebSocket" : "Fetch" } }); }), g("endRequest", ({ data: n }) => { const o = new L(n); c(o); }), g("responseData", ({ data: n }) => { const { id: o, rawData: u, statusCode: d, headers: h } = n, l = i(o), m = Buffer.from(u); l && (l.responseInfo.encodedDataLength = m.length, p(m, (f) => { l.responseData = f, l.responseInfo.dataLength = f.length, l.responseStatusCode = d, l.responseHeaders = new R(h).getData(), a(l), c(l); })); }), { getRequest: i, resourceService: r }; }), ye = q("websocket", ({ devtool: t, core: e }) => { const s = e.usePlugin("network"); g( "Network.webSocketCreated", async ({ data: { response: r, requestId: i } }) => { if (!i) return; const a = s.getRequest(i); if (!a) return; const c = te(r.rawHeaders); await t.send({ method: "Network.webSocketCreated", params: { url: a.url, initiator: a.initiator, requestId: a.id } }), await t.send({ method: "Network.webSocketWillSendHandshakeRequest", params: { wallTime: Date.now(), timestamp: t.getTimestamp(), requestId: a.id, request: { headers: a.requestHeaders } } }), await t.send({ method: "Network.webSocketHandshakeResponseReceived", params: { requestId: a.id, response: { headers: c, headersText: j( `HTTP/${r.httpVersion} ${r.statusCode} ${r.statusMessage}\r `, c ), status: r.statusCode, statusText: r.statusMessage, requestHeadersText: j( `${a.method} ${a.url} HTTP/${r.httpVersion}\r `, a.requestHeaders ), requestHeaders: se(a.requestHeaders) }, timestamp: t.getTimestamp() } }); } ), g("Network.webSocketFrameSent", async ({ data: r }) => { r.requestId && await t.send({ method: "Network.webSocketFrameSent", params: { requestId: r.requestId, response: r.response, // Network中数据时间戳 timestamp: E() } }); }), g("Network.webSocketFrameReceived", async ({ data: r }) => { r.requestId && await t.send({ method: "Network.webSocketFrameReceived", params: { requestId: r.requestId, response: r.response, timestamp: E() } }); }), g("Network.webSocketClosed", async ({ data: r }) => { r.requestId && await t.send({ method: "Network.webSocketClosed", params: { requestId: r.requestId, timestamp: E() } }); }); }), Ie = q("health", () => { const t = () => { process.exit(0); }; let e = setTimeout(t, 5e3); g("healthcheck", () => { clearTimeout(e), e = setTimeout(t, 5e3); }); }), ke = (t) => { t.loadPlugins([Se, le, ye, Ie]); }, M = oe(process.env.NETWORK_OPTIONS || "{}", {}), A = () => { const t = new ce({ serverPort: M.serverPort || re, port: M.port || z, autoOpenDevtool: M.autoOpenDevtool }); return ke(t), t; }; let W = A(), O = 0; const De = 5, Te = 30 * 1e3, G = () => { setTimeout( () => { if (O++, O >= De) { console.error("Restart limit reached"), T(); return; } W.close(), W = A(); }, 10 + Math.random() * 100 ); }; setInterval(() => { O = 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(); });