UNPKG

node-network-devtools

Version:

Inspecting Node.js's Network with Chrome DevTools

607 lines (606 loc) 17.7 kB
var H = (t) => { throw TypeError(t); }; var U = (t, e, s) => e.has(t) || H("Cannot " + s); var D = (t, e, s) => (U(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), b = (t, e, s, r) => (U(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 S, I as X, c as x, _ as Z, P as z, a as ee, R as P, p as te, f as j, s as se, g as L, S as re } from "./common-BUk9ORDJ.mjs"; import w from "fs"; import E from "path"; import { fileURLToPath as ne, pathToFileURL as R } from "url"; import _ from "iconv-lite"; import N from "node:zlib"; const oe = (t, e) => { try { return JSON.parse(t); } catch { return e; } }, ie = (t) => t.split(";")[0] || "text/plain"; class ae { constructor(e) { this.browser = null, this.timestamp = 0, this.startTime = Date.now(), this.listeners = []; const { port: s, autoOpenDevtool: r = !0, onConnect: c, onClose: i } = e; this.port = s, this.server = new J({ port: s }); const { server: u } = this; u.on("listening", () => { S(`devtool server is listening on port ${s}`), r && this.open(); }), this.socket = new Promise((o) => { u.on("connection", (n) => { c?.(), this.socket.then((a) => { a[0] = n; }), S("devtool connected"), n.on("message", (a) => { const p = JSON.parse(a.toString()); this.listeners.forEach((d) => d(null, p)); }), n.on("close", () => { S("devtool closed"), i?.(); }), n.on("error", (a) => { this.listeners.forEach((p) => p(a)); }), o([n]); }); }); } getTimestamp() { return this.updateTimestamp(), this.timestamp; } updateTimestamp() { this.timestamp = (Date.now() - this.startTime) / 1e3; } async open() { const e = `devtools://devtools/bundled/inspector.html?ws=localhost:${this.port}`; try { if (X) { S(`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=${x}` : "" ] }, wait: !0 }); e: if (process.platform !== "darwin") { const r = await new Promise( (n) => { let a = 0, p = setInterval(async () => { a > 5 && (clearInterval(p), n([])); try { a++, n((await fetch(`http://localhost:${x}/json`)).json()), clearInterval(p); } catch { } }, 500); } ), [c] = r; if (!c) break e; const { id: i, webSocketDebuggerUrl: u } = c, o = new K(u); o.on("open", () => { const n = { id: i, method: "Page.navigate", params: { url: e } }; o.send(JSON.stringify(n)), o.close(); }); } return S("opened in chrome or click here to open chrome devtool: ", e), this.browser = s, s; } 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)); } on(e) { this.listeners.push(e); } } function ce(t) { switch (t.split(".").pop()?.toLowerCase()) { case "js": case "mjs": case "cjs": return "JavaScript"; case "wasm": return "WebAssembly"; default: return "Unknown"; } } class ue { 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 pe { constructor() { this.scriptMap = new ue(), 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 w.readFileSync(r, "utf-8"); } catch (c) { return console.error("Error reading file:", c), null; } } /** * @description Read the last lines of the file * @param filePath * @param stat * @param totalLines * @returns string */ readLastLine(e, s, r = 1) { const c = s.size, i = Math.min(1024, c); let u = c - i, o = Buffer.alloc(i), n = []; const a = w.openSync(e, "r"); for (; n.length < r && u >= 0; ) w.readSync(a, o, 0, i, u), n = o.toString("utf8").split(` `).concat(n), u -= i, u < 0 && (u = 0, o = Buffer.alloc(c - u)); return w.closeSync(a), n.slice(-r).join(` `); } traverseDirToMap(e, s = ["node_modules"]) { const r = [], c = [e]; let i = this.scriptIdCounter; for (; c.length > 0; ) { const u = c.pop(), o = w.readdirSync(u); for (const n of o) { if (s.includes(n)) continue; const a = E.join(u, n), p = w.statSync(a); if (p.isDirectory()) c.push(a); else { const d = E.resolve(a), l = R(d), h = `${++i}`; let f = ""; if (/\.(js|ts)$/.test(d)) { const T = this.readLastLine(a, p, 2).match(/sourceMappingURL=(.+)$/m)?.[1] ?? ""; f = T ? T.startsWith("data:") ? ( // inline sourcemap T ) : ( // file path R(E.join(u, T)).href ) : ""; } const q = l.href, V = ce(q); r.push({ url: q, scriptLanguage: V, embedderName: l.href, scriptId: h, sourceMapURL: f, hasSourceURL: !!f }), this.scriptMap.addMapping(q, h); } } } return this.scriptIdCounter += r.length, r; } getLocalScriptList() { const e = this.traverseDirToMap(process.cwd()), s = this.traverseDirToMap(Z); return [...e, ...s]; } } var y, g; class de { constructor(e) { C(this, y); C(this, g); this.listeners = {}, b(this, y, []), this.options = e; const { serverPort: s, requests: r, autoOpenDevtool: c } = e; this.devtool = new ae({ port: s, autoOpenDevtool: c, onConnect: () => { this.listeners.onConnect?.forEach( (u) => u({ data: null, id: "onConnect" }) ); } }), this.resourceService = new pe(), this.devtool.on((i, u) => { if (i) { S(i); return; } const o = this.listeners[u.method]; o && o.forEach((n) => { n({ data: u.params, id: u.id }); }); }), this.server = this.initServer(); } loadPlugins(e) { b(this, y, e), b(this, g, /* @__PURE__ */ new Map()), e.forEach((s) => { const r = s({ devtool: this.devtool, core: this, plugins: D(this, y) }); D(this, g).set(s.id, r); }); } usePlugin(e) { return D(this, g) ? D(this, g).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) { console.warn("unknown message type", i.type); break; } u.forEach((o) => { o({ data: i.data }); }); } break; } }); }), e.on("listening", () => { process.send && process.send(ee); }), e; } } y = new WeakMap(), g = new WeakMap(); let I = null; const le = (t, e) => { I = { devtool: e, core: t }; }, he = () => I = null, O = (t, e) => Object.assign((c) => { le(c.core, c.devtool); const i = e(c); return he(), i; }, { id: t }), m = (t, e) => { if (!I) return; const s = I, { core: r } = s; return r.on(t, e); }, me = (t) => { if (!I) return; const { core: e } = I; return e.on("onConnect", t); }, fe = O("debugger", ({ devtool: t, core: e }) => { m("Debugger.getScriptSource", ({ id: r, data: c }) => { if (!r) return; const { scriptId: i } = c, u = e.resourceService.getScriptSource(i); t.send({ id: r, method: "Debugger.getScriptSourceResponse", result: { scriptSource: u } }); }); const s = e.resourceService.getLocalScriptList(); me(() => { s.forEach((r) => { t.send({ method: "Debugger.scriptParsed", params: r }); }); }); }); class v { 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 c of r) if (c.toLowerCase() === s) return this.headers[c]; } getData() { return this.headers; } valueOf() { return this.headers; } } class ge { constructor(e) { this.req = e; } decodeBody() { const { req: e } = this, r = new v(e.responseHeaders).getHeader("content-type") || "text/plain; charset=utf-8", c = r.match(/charset=([^;]+)/), i = c ? c[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 }; } } const $ = "517.528", F = "517.529", we = O("network", ({ devtool: t, core: e }) => { const s = {}, r = (o) => s[o], c = (o) => { s[o.id] = o; }, i = (o) => { o.requestEndTime = o.requestEndTime || Date.now(), t.updateTimestamp(); const a = new v(o.responseHeaders).getHeader("content-type") || "text/plain; charset=utf-8", p = /image/.test(a) ? "Image" : /javascript/.test(a) ? "Script" : /css/.test(a) ? "Stylesheet" : /html/.test(a) ? "Document" : "Other"; t.send({ method: "Network.responseReceived", params: { requestId: o.id, frameId: $, loaderId: F, timestamp: t.timestamp, type: p, response: { url: o.url, status: o.responseStatusCode, statusText: o.responseStatusCode === 200 ? "OK" : "", headers: o.responseHeaders, connectionReused: !1, encodedDataLength: o.responseInfo.encodedDataLength, charset: "utf-8", mimeType: ie(a) } } }), t.updateTimestamp(), t.send({ method: "Network.dataReceived", params: { requestId: o.id, timestamp: t.timestamp, dataLength: o.responseInfo.dataLength, encodedDataLength: o.responseInfo.encodedDataLength } }), t.updateTimestamp(), t.send({ method: "Network.loadingFinished", params: { requestId: o.id, timestamp: t.timestamp, encodedDataLength: o.responseInfo.encodedDataLength } }); }, u = (o, n) => { const a = [N.gunzip, N.inflate, N.brotliDecompress]; let p = 0; const d = () => { if (p >= a.length) { n(o); return; } const l = a[p]; p += 1, l(o, (h, f) => { h ? d() : n(f); }); }; d(); }; return m("Network.getResponseBody", ({ data: o, id: n }) => { const a = r(o.requestId); if (!n || !a) { console.error("request is not found"); return; } const p = new ge(a).decodeBody(); t.send({ id: n, result: p }); }), m("initRequest", ({ data: o }) => { const n = new P(o); n.initiator && n.initiator.stack.callFrames.forEach((a) => { const p = R(a.url), d = e.resourceService.getScriptIdByUrl(p.href) ?? e.resourceService.getScriptIdByUrl(a.url); d && (a.scriptId = d); }), s[n.id] = n; }), m("registerRequest", ({ data: o }) => { const n = new P(o); s[n.id] = n, n.initiator && n.initiator.stack.callFrames.forEach((d) => { const l = R(d.url), h = e.resourceService.getScriptIdByUrl(l.href) ?? e.resourceService.getScriptIdByUrl(d.url); h && (d.scriptId = h); }), t.updateTimestamp(); const a = new v(n.requestHeaders), p = a.getHeader("content-type"); return t.send({ method: "Network.requestWillBeSent", params: { requestId: n.id, frameId: $, loaderId: F, request: { url: n.url, method: n.method, headers: a.getData(), initialPriority: "High", mixedContentType: "none", ...n.requestData ? { postData: p?.includes("application/json") ? JSON.stringify(n.requestData) : n.requestData } : {} }, timestamp: t.timestamp, wallTime: n.requestStartTime, initiator: n.initiator, type: n.isWebSocket() ? "WebSocket" : "Fetch" } }); }), m("endRequest", ({ data: o }) => { const n = new P(o); i(n); }), m("responseData", ({ data: o }) => { const { id: n, rawData: a, statusCode: p, headers: d } = o, l = r(n), h = Buffer.from(a); l && (l.responseInfo.encodedDataLength = h.length, u(h, (f) => { l.responseData = f, l.responseInfo.dataLength = f.length, l.responseStatusCode = p, l.responseHeaders = new v(d).getData(), c(l), i(l); })); }), { getRequest: r }; }), Se = O("websocket", ({ devtool: t, core: e }) => { const s = e.usePlugin("network"); m( "Network.webSocketCreated", async ({ data: { response: r, requestId: c } }) => { if (!c) return; const i = s.getRequest(c); 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() } }); }); }), ye = (t) => { t.loadPlugins([we, fe, Se]); }, B = oe(process.env.NETWORK_OPTIONS || "{}", {}), A = () => { const t = new de({ serverPort: B.serverPort || re, port: B.port || z, autoOpenDevtool: B.autoOpenDevtool }); return ye(t), t; }; let W = A(), M = 0; const Ie = 5, De = 30 * 1e3, G = () => { setTimeout( () => { if (M++, M >= Ie) { console.error("Restart limit reached"), k(); return; } W.close(), W = A(); }, 10 + Math.random() * 100 ); }; setInterval(() => { M = 0; }, De); const k = () => { process.exit(0); }; process.on("exit", k); process.on("SIGINT", k); process.on("SIGTERM", k); process.on("beforeExit", k); process.on("uncaughtException", (t) => { console.error("uncaughtException: ", t), G(); }); process.on("unhandledRejection", (t) => { console.error("unhandledRejection: ", t), G(); });