@hitarth-gg/devtron
Version:
Electron DevTools Extension to track IPC events
558 lines (537 loc) • 23 kB
JavaScript
import * as __WEBPACK_EXTERNAL_MODULE_electron__ from "electron";
import { createRequire as __WEBPACK_EXTERNAL_createRequire } from "node:module";
/******/ var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/***/ ((module) => {
module.exports = __WEBPACK_EXTERNAL_MODULE_electron__;
/***/ }),
/* 2 */
/***/ ((module) => {
module.exports = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:path");
/***/ }),
/* 3 */
/***/ ((module) => {
module.exports = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("node:module");
/***/ }),
/* 4 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ MSG_TYPE: () => (/* binding */ MSG_TYPE),
/* harmony export */ PORT_NAME: () => (/* binding */ PORT_NAME),
/* harmony export */ excludedIpcChannels: () => (/* binding */ excludedIpcChannels)
/* harmony export */ });
const PORT_NAME = {
PANEL: 'devt-panel',
CONTENT_SCRIPT: 'devt-content-script',
};
const MSG_TYPE = {
PING: 'ping',
PONG: 'pong',
GET_ALL_EVENTS: 'get-all-events',
RENDER_EVENT: 'render-event',
CLEAR_EVENTS: 'clear-events',
EVENTS_CLEARED_ACK: 'events-cleared-ack',
ADD_IPC_EVENT: 'add-ipc-event',
SEND_TO_PANEL: 'send-to-panel',
};
/**
* These channels are used internally by Devtron, and tracking them may lead to unnecessary noise.
* Hence, they are ignored by Devtron.
*/
const excludedIpcChannels = ['devtron-ipc-events'];
/***/ }),
/* 5 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ logger: () => (/* binding */ logger)
/* harmony export */ });
class Logger {
logLevelMap = {
debug: 1,
info: 2,
warn: 3,
error: 4,
none: 5,
};
currentLogLevel = 'debug';
currentLogLevelIndex = this.logLevelMap[this.currentLogLevel];
setLogLevel(level) {
if (this.currentLogLevel === level)
return; // no change
if (this.logLevelMap[level] === undefined) {
console.error(`Invalid log level: ${level}`);
return;
}
this.currentLogLevel = level;
this.currentLogLevelIndex = this.logLevelMap[level];
}
log(level, ...args) {
if (this.currentLogLevel === 'none')
return;
if (this.logLevelMap[level] < this.currentLogLevelIndex)
return;
switch (level) {
case 'debug':
console.debug(...args);
break;
case 'info':
console.log(...args);
break;
case 'warn':
console.warn(...args);
break;
case 'error':
console.error(...args);
break;
}
}
debug(...args) {
this.log('debug', ...args);
}
info(...args) {
this.log('info', ...args);
}
warn(...args) {
this.log('warn', ...args);
}
error(...args) {
this.log('error', ...args);
}
}
const logger = new Logger();
/***/ })
/******/ ]);
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/compat get default export */
/******/ (() => {
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = (module) => {
/******/ var getter = module && module.__esModule ?
/******/ () => (module['default']) :
/******/ () => (module);
/******/ __webpack_require__.d(getter, { a: getter });
/******/ return getter;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
(() => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ devtron: () => (/* binding */ devtron)
/* harmony export */ });
/* harmony import */ var electron__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(node_path__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var node_module__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
/* harmony import */ var node_module__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(node_module__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _common_constants__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
/* harmony import */ var _utils_Logger__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5);
let isInstalled = false;
let isInstalledToDefaultSession = false;
let devtronSW;
const isPayloadWithUuid = (payload) => {
// If the first argument is an object with __uuid__devtron then it is a custom payload
return (payload[0] &&
typeof payload[0] === 'object' &&
payload[0].__uuid__devtron &&
Array.isArray(payload[0].args));
};
const getArgsFromPayload = (payload) => {
if (isPayloadWithUuid(payload)) {
// If the payload is a custom payload, return the args array
return payload[0].args || [];
}
// Otherwise, return the payload as is
return payload;
};
const getUuidFromPayload = (payload) => {
if (isPayloadWithUuid(payload)) {
return payload[0].__uuid__devtron;
}
return '';
};
/**
* sends captured IPC events to the service-worker preload script
*/
function trackIpcEvent({ direction, channel, args, devtronSW, serviceWorkerDetails, method, }) {
if (_common_constants__WEBPACK_IMPORTED_MODULE_3__.excludedIpcChannels.includes(channel))
return;
if (!devtronSW) {
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn('The service-worker for Devtron is not registered yet. Cannot track IPC event.');
return;
}
const uuid = getUuidFromPayload(args);
const newArgs = getArgsFromPayload(args);
const eventData = {
direction,
channel,
args: newArgs,
timestamp: Date.now(),
serviceWorkerDetails,
};
if (method)
eventData.method = method;
if (uuid)
eventData.uuid = uuid;
devtronSW.send('devtron-render-event', eventData);
}
function registerIpcListeners(ses, devtronSW) {
ses.on(
// @ts-expect-error: '-ipc-message' is an internal event
'-ipc-message', (event, channel, args) => {
if (event.type === 'frame')
trackIpcEvent({ direction: 'renderer-to-main', channel, args, devtronSW });
else if (event.type === 'service-worker')
trackIpcEvent({ direction: 'service-worker-to-main', channel, args, devtronSW });
});
ses.on(
// @ts-expect-error: '-ipc-invoke' is an internal event
'-ipc-invoke', (event, channel, args) => {
if (event.type === 'frame')
trackIpcEvent({ direction: 'renderer-to-main', channel, args, devtronSW });
else if (event.type === 'service-worker')
trackIpcEvent({ direction: 'service-worker-to-main', channel, args, devtronSW });
});
ses.on(
// @ts-expect-error: '-ipc-message-sync' is an internal event
'-ipc-message-sync', (event, channel, args) => {
if (event.type === 'frame')
trackIpcEvent({ direction: 'renderer-to-main', channel, args, devtronSW });
else if (event.type === 'service-worker')
trackIpcEvent({ direction: 'service-worker-to-main', channel, args, devtronSW });
});
}
/**
* Registers a listener for the service worker's send method to track IPC events
* sent from the main process to the service worker.
*/
function registerServiceWorkerSendListener(ses, devtronSW) {
const isInstalledSet = new Set(); // stores version IDs of patched service workers
// register listener for existing service workers
const allRunning = ses.serviceWorkers.getAllRunning();
for (const vid in allRunning) {
const swInfo = allRunning[vid];
const sw = ses.serviceWorkers.getWorkerFromVersionID(Number(vid));
if (typeof sw === 'undefined' || sw.scope === devtronSW.scope)
continue;
isInstalledSet.add(swInfo.versionId);
const originalSend = sw.send;
sw.send = function (...args) {
trackIpcEvent({
direction: 'main-to-service-worker',
channel: args[0],
args: args.slice(1),
devtronSW,
serviceWorkerDetails: {
serviceWorkerScope: sw.scope,
serviceWorkerVersionId: sw.versionId,
},
});
return originalSend.apply(this, args);
};
}
// register listener for new service workers
ses.serviceWorkers.on('running-status-changed', (details) => {
if (details.runningStatus === 'running' || details.runningStatus === 'starting') {
const sw = ses.serviceWorkers.getWorkerFromVersionID(details.versionId);
if (typeof sw === 'undefined' ||
sw.scope === devtronSW.scope ||
isInstalledSet.has(sw.versionId))
return;
isInstalledSet.add(details.versionId);
const originalSend = sw.send;
sw.send = function (...args) {
trackIpcEvent({
direction: 'main-to-service-worker',
channel: args[0],
args: args.slice(1),
devtronSW,
serviceWorkerDetails: {
serviceWorkerScope: sw.scope,
serviceWorkerVersionId: sw.versionId,
},
});
return originalSend.apply(this, args);
};
}
});
}
async function startServiceWorker(ses, extension) {
try {
const sw = await ses.serviceWorkers.startWorkerForScope(extension.url);
sw.startTask();
devtronSW = sw;
registerIpcListeners(ses, sw);
registerServiceWorkerSendListener(ses, sw);
}
catch (error) {
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn(`Failed to start Devtron service-worker (${error}), trying again...`);
/**
* This is a workaround for the issue where the Devtron service-worker fails to start
* when the Electron app is launched for the first time, or when the service worker
* hasn't been cached yet.
*/
try {
const handleDetails = async (event, details) => {
if (details.scope === extension.url) {
const sw = await ses.serviceWorkers.startWorkerForScope(extension.url);
sw.startTask();
devtronSW = sw;
registerIpcListeners(ses, sw);
registerServiceWorkerSendListener(ses, sw);
ses.serviceWorkers.removeListener('registration-completed', handleDetails);
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.info(`Devtron service-worker started successfully`);
}
};
ses.serviceWorkers.on('registration-completed', handleDetails);
}
catch (error) {
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.error('Failed to start Devtron service-worker:', error);
}
}
}
function patchIpcMain() {
const listenerMap = new Map(); // channel -> (originalListener -> tracked/cleaned Listener)
const storeTrackedListener = (channel, original, tracked) => {
if (!listenerMap.has(channel)) {
listenerMap.set(channel, new Map());
}
listenerMap.get(channel).set(original, tracked);
};
const originalOn = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.on.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalOff = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.off.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalOnce = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.once.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalAddListener = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.addListener.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalRemoveListener = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeListener.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalRemoveAllListeners = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeAllListeners.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalHandle = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handle.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalHandleOnce = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handleOnce.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
const originalRemoveHandler = electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeHandler.bind(electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain);
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.on = (channel, listener) => {
const cleanedListener = (event, ...args) => {
const newArgs = getArgsFromPayload(args);
listener(event, ...newArgs);
};
storeTrackedListener(channel, listener, cleanedListener);
return originalOn(channel, cleanedListener);
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.off = (channel, listener) => {
const channelMap = listenerMap.get(channel);
const cleanedListener = channelMap?.get(listener);
if (!cleanedListener)
return electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain;
channelMap?.delete(listener);
if (channelMap && channelMap.size === 0) {
listenerMap.delete(channel);
}
trackIpcEvent({ direction: 'main', channel, args: [], devtronSW, method: 'off' });
return originalOff(channel, cleanedListener);
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.once = (channel, listener) => {
const cleanedListener = (event, ...args) => {
const newArgs = getArgsFromPayload(args);
listener(event, ...newArgs);
};
return originalOnce(channel, cleanedListener);
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.addListener = (channel, listener) => {
const cleanedListener = (event, ...args) => {
const newArgs = getArgsFromPayload(args);
listener(event, ...newArgs);
};
storeTrackedListener(channel, listener, cleanedListener);
return originalAddListener(channel, cleanedListener);
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeListener = (channel, listener) => {
const channelMap = listenerMap.get(channel);
const cleanedListener = channelMap?.get(listener);
if (!cleanedListener)
return electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain;
// Remove the listener from the map
channelMap?.delete(listener);
// If no listeners left for this channel, remove the channel from the map
if (channelMap && channelMap.size === 0) {
listenerMap.delete(channel);
}
trackIpcEvent({ direction: 'main', channel, args: [], devtronSW, method: 'removeListener' });
return originalRemoveListener(channel, cleanedListener);
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeAllListeners = (channel) => {
if (channel) {
listenerMap.delete(channel);
trackIpcEvent({
direction: 'main',
channel,
args: [],
devtronSW,
method: 'removeAllListeners',
});
return originalRemoveAllListeners(channel);
}
else {
listenerMap.clear();
trackIpcEvent({
direction: 'main',
channel: '',
args: [],
devtronSW,
method: 'removeAllListeners',
});
listenerMap.clear();
return originalRemoveAllListeners();
}
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handle = (channel, listener) => {
const cleanedListener = async (event, ...args) => {
const newArgs = getArgsFromPayload(args);
const result = await listener(event, ...newArgs);
return result;
};
return originalHandle(channel, cleanedListener);
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.handleOnce = (channel, listener) => {
const cleanedListener = async (event, ...args) => {
const newArgs = getArgsFromPayload(args);
const result = await listener(event, ...newArgs);
return result;
};
return originalHandleOnce(channel, cleanedListener);
};
electron__WEBPACK_IMPORTED_MODULE_0__.ipcMain.removeHandler = (channel) => {
listenerMap.delete(channel);
trackIpcEvent({ direction: 'main', channel, args: [], devtronSW, method: 'removeHandler' });
return originalRemoveHandler(channel);
};
}
async function install(options = {}) {
if (isInstalled)
return;
isInstalled = true;
// set log level
if (options.logLevel)
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.setLogLevel(options.logLevel);
patchIpcMain();
const installToSession = async (ses) => {
if (ses === electron__WEBPACK_IMPORTED_MODULE_0__.session.defaultSession && isInstalledToDefaultSession)
return;
if (ses === electron__WEBPACK_IMPORTED_MODULE_0__.session.defaultSession)
isInstalledToDefaultSession = true;
let devtron;
try {
// register service worker preload script
const dirname = import.meta.url; // __dirname is replaced with import.meta.url in ESM builds using webpack
const serviceWorkerPreloadPath = (0,node_module__WEBPACK_IMPORTED_MODULE_2__.createRequire)(dirname).resolve('@hitarth-gg/devtron/service-worker-preload');
const rendererPreloadPath = (0,node_module__WEBPACK_IMPORTED_MODULE_2__.createRequire)(dirname).resolve('@hitarth-gg/devtron/renderer-preload');
ses.registerPreloadScript({
filePath: serviceWorkerPreloadPath,
type: 'service-worker',
id: 'devtron-sw-preload',
});
ses.registerPreloadScript({
filePath: rendererPreloadPath,
type: 'frame',
id: 'devtron-renderer-preload',
});
// load extension
const extensionPath = node_path__WEBPACK_IMPORTED_MODULE_1___default().resolve(serviceWorkerPreloadPath, '..', '..', 'extension');
devtron = await ses.extensions.loadExtension(extensionPath, { allowFileAccess: true });
await startServiceWorker(ses, devtron);
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.info('Devtron loaded successfully');
}
catch (error) {
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.error('Failed to load Devtron:', error);
}
};
electron__WEBPACK_IMPORTED_MODULE_0__.app.on('session-created', installToSession);
// explicitly install Devtron to the defaultSession in case the app is already ready
if (!isInstalledToDefaultSession && electron__WEBPACK_IMPORTED_MODULE_0__.app.isReady())
await installToSession(electron__WEBPACK_IMPORTED_MODULE_0__.session.defaultSession);
}
/**
* Retrieves the list of IPC events tracked by Devtron.
*
* - If called before installation or before the Devtron service worker is ready,
* an empty array will be returned.
*/
async function getEvents() {
if (!isInstalled) {
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn('You are trying to get IPC events before Devtron is installed.');
return [];
}
if (!devtronSW) {
_utils_Logger__WEBPACK_IMPORTED_MODULE_4__.logger.warn('Devtron service-worker is not registered yet. Cannot get IPC events.');
return [];
}
devtronSW.send('devtron-get-ipc-events');
return new Promise((resolve) => {
devtronSW.ipc.once('devtron-ipc-events', (event, ipcEvents) => {
resolve(ipcEvents);
});
});
}
const devtron = {
install,
getEvents,
};
})();
const __webpack_exports__devtron = __webpack_exports__.devtron;
export { __webpack_exports__devtron as devtron };