metro-inspector-proxy
Advanced tools
Comparing version 0.73.3 to 0.73.5
{ | ||
"name": "metro-inspector-proxy", | ||
"version": "0.73.3", | ||
"version": "0.73.5", | ||
"description": "🚇 Inspector proxy for React Native and dev tools integration.", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
@@ -11,8 +11,7 @@ /** | ||
*/ | ||
"use strict"; | ||
const { runInspectorProxy } = require("./index"); | ||
const yargs = require("yargs"); | ||
const argv = yargs | ||
@@ -19,0 +18,0 @@ .option("port", { |
"use strict"; | ||
var fs = _interopRequireWildcard(require("fs")); | ||
var http = _interopRequireWildcard(require("http")); | ||
var path = _interopRequireWildcard(require("path")); | ||
var _ws = _interopRequireDefault(require("ws")); | ||
function _interopRequireDefault(obj) { | ||
return obj && obj.__esModule ? obj : { default: obj }; | ||
} | ||
function _getRequireWildcardCache(nodeInterop) { | ||
@@ -23,3 +18,2 @@ if (typeof WeakMap !== "function") return null; | ||
} | ||
function _interopRequireWildcard(obj, nodeInterop) { | ||
@@ -57,3 +51,2 @@ if (!nodeInterop && obj && obj.__esModule) { | ||
} | ||
/** | ||
@@ -69,11 +62,14 @@ * Copyright (c) Meta Platforms, Inc. and affiliates. | ||
*/ | ||
const debug = require("debug")("Metro:InspectorProxy"); | ||
const PAGES_POLLING_INTERVAL = 1000; | ||
const PAGES_POLLING_INTERVAL = 1000; // Android's stock emulator and other emulators such as genymotion use a standard localhost alias. | ||
// Android's stock emulator and other emulators such as genymotion use a standard localhost alias. | ||
const EMULATOR_LOCALHOST_ADDRESSES = ["10.0.2.2", "10.0.3.2"]; | ||
const EMULATOR_LOCALHOST_ADDRESSES = ["10.0.2.2", "10.0.3.2"]; // Prefix for script URLs that are alphanumeric IDs. See comment in _processMessageFromDevice method for | ||
// Prefix for script URLs that are alphanumeric IDs. See comment in _processMessageFromDevice method for | ||
// more details. | ||
const FILE_PREFIX = "file://"; | ||
const REACT_NATIVE_RELOADABLE_PAGE_ID = "-1"; | ||
/** | ||
@@ -83,21 +79,31 @@ * Device class represents single device connection to Inspector Proxy. Each device | ||
*/ | ||
class Device { | ||
// ID of the device. | ||
// Name of the device. | ||
// Package name of the app. | ||
// Stores socket connection between Inspector Proxy and device. | ||
// Stores last list of device's pages. | ||
// Stores information about currently connected debugger (if any). | ||
_debuggerConnection = null; // Last known Page ID of the React Native page. | ||
_debuggerConnection = null; | ||
// Last known Page ID of the React Native page. | ||
// This is used by debugger connections that don't have PageID specified | ||
// (and will interact with the latest React Native page). | ||
_lastConnectedReactNativePage = null; | ||
_lastConnectedReactNativePage = null; // Whether we are in the middle of a reload in the REACT_NATIVE_RELOADABLE_PAGE. | ||
// Whether we are in the middle of a reload in the REACT_NATIVE_RELOADABLE_PAGE. | ||
_isReloading = false; | ||
_isReloading = false; // The previous "GetPages" message, for deduplication in debug logs. | ||
// The previous "GetPages" message, for deduplication in debug logs. | ||
_lastGetPagesMessage = ""; | ||
_lastGetPagesMessage = ""; // Mapping built from scriptParsed events and used to fetch file content in `Debugger.getScriptSource`. | ||
// Mapping built from scriptParsed events and used to fetch file content in `Debugger.getScriptSource`. | ||
_scriptIdToSourcePathMapping = new Map(); | ||
_scriptIdToSourcePathMapping = new Map(); // Root of the project used for relative to absolute source path conversion. | ||
// Root of the project used for relative to absolute source path conversion. | ||
@@ -111,6 +117,4 @@ constructor(id, name, app, socket, projectRoot) { | ||
this._projectRoot = projectRoot; | ||
this._deviceSocket.on("message", (message) => { | ||
const parsedMessage = JSON.parse(message); | ||
if (parsedMessage.event === "getPages") { | ||
@@ -128,6 +132,4 @@ // There's a 'getPages' message every second, so only show them if they change | ||
} | ||
this._handleMessageFromDevice(parsedMessage); | ||
}); | ||
this._deviceSocket.on("close", () => { | ||
@@ -137,14 +139,10 @@ // Device disconnected - close debugger connection. | ||
this._debuggerConnection.socket.close(); | ||
this._debuggerConnection = null; | ||
} | ||
}); | ||
this._setPagesPolling(); | ||
} | ||
getName() { | ||
return this._name; | ||
} | ||
getPagesList() { | ||
@@ -162,7 +160,8 @@ if (this._lastConnectedReactNativePage) { | ||
} | ||
} // Handles new debugger connection to this device: | ||
} | ||
// Handles new debugger connection to this device: | ||
// 1. Sends connect event to device | ||
// 2. Forwards all messages from the debugger to device as wrappedEvent | ||
// 3. Sends disconnect event to device when debugger connection socket closes. | ||
handleDebuggerConnection(socket, pageId) { | ||
@@ -172,6 +171,4 @@ // Disconnect current debugger if we already have debugger connected. | ||
this._debuggerConnection.socket.close(); | ||
this._debuggerConnection = null; | ||
} | ||
const debuggerInfo = { | ||
@@ -184,3 +181,2 @@ socket, | ||
debug(`Got new debugger connection for page ${pageId} of ${this._name}`); | ||
this._sendMessageToDevice({ | ||
@@ -192,7 +188,5 @@ event: "connect", | ||
}); | ||
socket.on("message", (message) => { | ||
debug("(Debugger) -> (Proxy) (Device): " + message); | ||
const debuggerRequest = JSON.parse(message); | ||
const handled = this._interceptMessageFromDebugger( | ||
@@ -203,3 +197,2 @@ debuggerRequest, | ||
); | ||
if (!handled) { | ||
@@ -217,3 +210,2 @@ this._sendMessageToDevice({ | ||
debug(`Debugger for page ${pageId} and ${this._name} disconnected.`); | ||
this._sendMessageToDevice({ | ||
@@ -225,7 +217,5 @@ event: "disconnect", | ||
}); | ||
this._debuggerConnection = null; | ||
}); | ||
const sendFunc = socket.send; | ||
socket.send = function (message) { | ||
@@ -235,3 +225,5 @@ debug("(Debugger) <- (Proxy) (Device): " + message); | ||
}; | ||
} // Handles messages received from device: | ||
} | ||
// Handles messages received from device: | ||
// 1. For getPages responses updates local _pages list. | ||
@@ -243,6 +235,7 @@ // 2. All other messages are forwarded to debugger as wrappedEvent. | ||
// locations). | ||
_handleMessageFromDevice(message) { | ||
if (message.event === "getPages") { | ||
this._pages = message.payload; // Check if device have new React Native page. | ||
this._pages = message.payload; | ||
// Check if device have new React Native page. | ||
// There is usually no more than 2-3 pages per device so this operation | ||
@@ -252,7 +245,5 @@ // is not expensive. | ||
// created instead of manually checking this on every getPages result. | ||
for (let i = 0; i < this._pages.length; ++i) { | ||
if (this._pages[i].title.indexOf("React") >= 0) { | ||
var _this$_lastConnectedR; | ||
if ( | ||
@@ -266,3 +257,2 @@ this._pages[i].id != | ||
this._newReactNativePage(this._pages[i]); | ||
break; | ||
@@ -279,3 +269,2 @@ } | ||
: null; | ||
if (debuggerSocket && debuggerSocket.readyState === _ws.default.OPEN) { | ||
@@ -297,7 +286,8 @@ if ( | ||
return; | ||
} // FIXME: Is it possible that we received message for pageID that does not | ||
} | ||
// FIXME: Is it possible that we received message for pageID that does not | ||
// correspond to current debugger connection? | ||
const debuggerSocket = this._debuggerConnection.socket; | ||
if ( | ||
@@ -310,5 +300,3 @@ debuggerSocket == null || | ||
} | ||
const parsedPayload = JSON.parse(message.payload.wrappedEvent); | ||
if (this._debuggerConnection) { | ||
@@ -318,8 +306,8 @@ // Wrapping just to make flow happy :) | ||
} | ||
const messageToSend = JSON.stringify(parsedPayload); | ||
debuggerSocket.send(messageToSend); | ||
} | ||
} // Sends single message to device. | ||
} | ||
// Sends single message to device. | ||
_sendMessageToDevice(message) { | ||
@@ -330,7 +318,7 @@ try { | ||
} | ||
this._deviceSocket.send(JSON.stringify(message)); | ||
} catch (error) {} | ||
} // Sends 'getPages' request to device every PAGES_POLLING_INTERVAL milliseconds. | ||
} | ||
// Sends 'getPages' request to device every PAGES_POLLING_INTERVAL milliseconds. | ||
_setPagesPolling() { | ||
@@ -344,9 +332,8 @@ setInterval( | ||
); | ||
} // We received new React Native Page ID. | ||
} | ||
// We received new React Native Page ID. | ||
_newReactNativePage(page) { | ||
var _this$_lastConnectedR2; | ||
debug(`React Native page updated to ${page.id}`); | ||
if ( | ||
@@ -362,3 +349,2 @@ this._debuggerConnection == null || | ||
} | ||
const oldPageId = | ||
@@ -370,3 +356,5 @@ (_this$_lastConnectedR2 = this._lastConnectedReactNativePage) === null || | ||
this._lastConnectedReactNativePage = page; | ||
this._isReloading = true; // We already had a debugger connected to React Native page and a | ||
this._isReloading = true; | ||
// We already had a debugger connected to React Native page and a | ||
// new one appeared - in this case we need to emulate execution context | ||
@@ -384,3 +372,2 @@ // detroy and resend Debugger.enable and Runtime.enable commands to new | ||
} | ||
this._sendMessageToDevice({ | ||
@@ -392,3 +379,2 @@ event: "connect", | ||
}); | ||
const toSend = [ | ||
@@ -404,3 +390,2 @@ { | ||
]; | ||
for (const message of toSend) { | ||
@@ -415,4 +400,5 @@ this._sendMessageToDevice({ | ||
} | ||
} // Allows to make changes in incoming message from device. | ||
} | ||
// Allows to make changes in incoming message from device. | ||
_processMessageFromDevice(payload, debuggerInfo) { | ||
@@ -422,7 +408,5 @@ // Replace Android addresses for scriptParsed event. | ||
const params = payload.params || {}; | ||
if ("sourceMapURL" in params) { | ||
for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) { | ||
const address = EMULATOR_LOCALHOST_ADDRESSES[i]; | ||
if (params.sourceMapURL.indexOf(address) >= 0) { | ||
@@ -437,7 +421,5 @@ payload.params.sourceMapURL = params.sourceMapURL.replace( | ||
} | ||
if ("url" in params) { | ||
for (let i = 0; i < EMULATOR_LOCALHOST_ADDRESSES.length; ++i) { | ||
const address = EMULATOR_LOCALHOST_ADDRESSES[i]; | ||
if (params.url.indexOf(address) >= 0) { | ||
@@ -447,12 +429,14 @@ payload.params.url = params.url.replace(address, "localhost"); | ||
} | ||
} // Chrome doesn't download source maps if URL param is not a valid | ||
} | ||
// Chrome doesn't download source maps if URL param is not a valid | ||
// URL. Some frameworks pass alphanumeric script ID instead of URL which causes | ||
// Chrome to not download source maps. In this case we want to prepend script ID | ||
// with 'file://' prefix. | ||
if (payload.params.url.match(/^[0-9a-z]+$/)) { | ||
payload.params.url = FILE_PREFIX + payload.params.url; | ||
debuggerInfo.prependedFilePrefix = true; | ||
} // $FlowFixMe[prop-missing] | ||
} | ||
// $FlowFixMe[prop-missing] | ||
if (params.scriptId != null) { | ||
@@ -462,3 +446,2 @@ this._scriptIdToSourcePathMapping.set(params.scriptId, params.url); | ||
} | ||
if (debuggerInfo.pageId == REACT_NATIVE_RELOADABLE_PAGE_ID) { | ||
@@ -470,3 +453,2 @@ // Chrome won't use the source map unless it appears to be new. | ||
} | ||
if (payload.params.url) { | ||
@@ -478,3 +460,2 @@ payload.params.url += | ||
} | ||
if ( | ||
@@ -491,3 +472,5 @@ payload.method === "Runtime.executionContextCreated" && | ||
}) | ||
); // The VM starts in a paused mode. Ask it to resume. | ||
); | ||
// The VM starts in a paused mode. Ask it to resume. | ||
// Note that if setting breakpoints in early initialization functions, | ||
@@ -499,3 +482,2 @@ // there's a currently race condition between these functions executing | ||
// at its convenience. | ||
this._sendMessageToDevice({ | ||
@@ -511,9 +493,9 @@ event: "wrappedEvent", | ||
}); | ||
this._isReloading = false; | ||
} | ||
} // Allows to make changes in incoming messages from debugger. Returns a boolean | ||
} | ||
// Allows to make changes in incoming messages from debugger. Returns a boolean | ||
// indicating whether the message has been handled locally (i.e. does not need | ||
// to be forwarded to the target). | ||
_interceptMessageFromDebugger(req, debuggerInfo, socket) { | ||
@@ -524,9 +506,6 @@ if (req.method === "Debugger.setBreakpointByUrl") { | ||
this._processDebuggerGetScriptSource(req, socket); | ||
return true; | ||
} | ||
return false; | ||
} | ||
_processDebuggerSetBreakpointByUrl(req, debuggerInfo) { | ||
@@ -540,3 +519,2 @@ // If we replaced Android emulator's address to localhost we need to change it back. | ||
); | ||
if ( | ||
@@ -552,6 +530,6 @@ req.params.url && | ||
} | ||
if (req.params.urlRegex) { | ||
req.params.urlRegex = req.params.urlRegex.replace( | ||
/localhost/g, // $FlowFixMe[incompatible-call] | ||
/localhost/g, | ||
// $FlowFixMe[incompatible-call] | ||
debuggerInfo.originalSourceURLAddress | ||
@@ -562,6 +540,4 @@ ); | ||
} | ||
_processDebuggerGetScriptSource(req, socket) { | ||
let scriptSource = `Source for script with id '${req.params.scriptId}' was not found.`; | ||
const sendResponse = () => { | ||
@@ -578,14 +554,10 @@ const result = { | ||
}; | ||
const pathToSource = this._scriptIdToSourcePathMapping.get( | ||
req.params.scriptId | ||
); | ||
if (pathToSource) { | ||
let pathIsURL = false; | ||
try { | ||
pathIsURL = new URL(pathToSource).hostname == "localhost"; | ||
} catch {} | ||
if (pathIsURL) { | ||
@@ -595,3 +567,2 @@ http | ||
const { statusCode } = httpResponse; | ||
if (statusCode == 200) { | ||
@@ -625,3 +596,2 @@ httpResponse.setEncoding("utf8"); | ||
} | ||
sendResponse(); | ||
@@ -631,3 +601,2 @@ } | ||
} | ||
_mapToDevicePageId(pageId) { | ||
@@ -644,3 +613,2 @@ if ( | ||
} | ||
module.exports = Device; |
@@ -11,20 +11,17 @@ /** | ||
*/ | ||
"use strict"; | ||
const InspectorProxy = require("./InspectorProxy"); | ||
const { parse } = require("url"); // Runs new HTTP Server and attaches Inspector Proxy to it. | ||
const { parse } = require("url"); | ||
// Runs new HTTP Server and attaches Inspector Proxy to it. | ||
// Requires are inlined here because we don't want to import them | ||
// when someone needs only InspectorProxy instance (without starting | ||
// new HTTP server). | ||
function runInspectorProxy(port, projectRoot) { | ||
const inspectorProxy = new InspectorProxy(projectRoot); | ||
const app = require("connect")(); // $FlowFixMe[method-unbinding] added when improving typing for this parameters | ||
const app = require("connect")(); | ||
// $FlowFixMe[method-unbinding] added when improving typing for this parameters | ||
app.use(inspectorProxy.processRequest.bind(inspectorProxy)); | ||
const httpServer = require("http").createServer(app); | ||
httpServer.listen(port, "127.0.0.1", () => { | ||
@@ -35,3 +32,2 @@ const websocketEndpoints = | ||
const { pathname } = parse(request.url); | ||
if (pathname != null && websocketEndpoints[pathname]) { | ||
@@ -52,3 +48,2 @@ websocketEndpoints[pathname].handleUpgrade( | ||
} | ||
module.exports = { | ||
@@ -55,0 +50,0 @@ InspectorProxy, |
@@ -11,12 +11,9 @@ /** | ||
*/ | ||
"use strict"; | ||
const Device = require("./Device"); | ||
const debug = require("debug")("Metro:InspectorProxy"); | ||
const url = require("url"); | ||
const WS = require("ws"); | ||
const WS_DEVICE_URL = "/inspector/device"; | ||
@@ -28,24 +25,27 @@ const WS_DEBUGGER_URL = "/inspector/debug"; | ||
const INTERNAL_ERROR_CODE = 1011; | ||
/** | ||
* Main Inspector Proxy class that connects JavaScript VM inside Android/iOS apps and JS debugger. | ||
*/ | ||
class InspectorProxy { | ||
// Root of the project used for relative to absolute source path conversion. | ||
// Maps device ID to Device instance. | ||
// Internal counter for device IDs -- just gets incremented for each new device. | ||
_deviceCounter = 0; // We store server's address with port (like '127.0.0.1:8081') to be able to build URLs | ||
_deviceCounter = 0; | ||
// We store server's address with port (like '127.0.0.1:8081') to be able to build URLs | ||
// (devtoolsFrontendUrl and webSocketDebuggerUrl) for page descriptions. These URLs are used | ||
// by debugger to know where to connect. | ||
_serverAddressWithPort = ""; | ||
constructor(projectRoot) { | ||
this._projectRoot = projectRoot; | ||
this._devices = new Map(); | ||
} // Process HTTP request sent to server. We only respond to 2 HTTP requests: | ||
} | ||
// Process HTTP request sent to server. We only respond to 2 HTTP requests: | ||
// 1. /json/version returns Chrome debugger protocol version that we use | ||
// 2. /json and /json/list returns list of page descriptions (list of inspectable apps). | ||
// This list is combined from all the connected devices. | ||
processRequest(request, response, next) { | ||
@@ -65,3 +65,2 @@ if ( | ||
}); | ||
this._sendJsonResponse(response, result); | ||
@@ -76,7 +75,7 @@ } else if (request.url === PAGES_LIST_JSON_VERSION_URL) { | ||
} | ||
} // Adds websocket listeners to the provided HTTP/HTTPS server. | ||
} | ||
// Adds websocket listeners to the provided HTTP/HTTPS server. | ||
createWebSocketListeners(server) { | ||
const { port } = server.address(); | ||
if (server.address().family === "IPv6") { | ||
@@ -87,3 +86,2 @@ this._serverAddressWithPort = `[::1]:${port}`; | ||
} | ||
return { | ||
@@ -93,5 +91,6 @@ [WS_DEVICE_URL]: this._createDeviceConnectionWSServer(), | ||
}; | ||
} // Converts page information received from device into PageDescription object | ||
} | ||
// Converts page information received from device into PageDescription object | ||
// that is sent to debugger. | ||
_buildPageDescription(deviceId, device, page) { | ||
@@ -113,5 +112,6 @@ const debuggerUrl = `${this._serverAddressWithPort}${WS_DEBUGGER_URL}?device=${deviceId}&page=${page.id}`; | ||
}; | ||
} // Sends object as response to HTTP request. | ||
} | ||
// Sends object as response to HTTP request. | ||
// Just serializes object using JSON and sets required headers. | ||
_sendJsonResponse(response, object) { | ||
@@ -126,3 +126,5 @@ const data = JSON.stringify(object, null, 2); | ||
response.end(data); | ||
} // Adds websocket handler for device connections. | ||
} | ||
// Adds websocket handler for device connections. | ||
// Device connects to /inspector/device and passes device and app names as | ||
@@ -132,3 +134,2 @@ // HTTP GET params. | ||
// new instance of Device class. | ||
_createDeviceConnectionWSServer() { | ||
@@ -138,4 +139,4 @@ const wss = new WS.Server({ | ||
perMessageDeflate: true, | ||
}); // $FlowFixMe[value-as-type] | ||
}); | ||
// $FlowFixMe[value-as-type] | ||
wss.on("connection", async (socket, req) => { | ||
@@ -147,3 +148,2 @@ try { | ||
const deviceId = this._deviceCounter++; | ||
this._devices.set( | ||
@@ -153,7 +153,5 @@ deviceId, | ||
); | ||
debug(`Got new connection: device=${deviceName}, app=${appName}`); | ||
socket.on("close", () => { | ||
this._devices.delete(deviceId); | ||
debug(`Device ${deviceName} disconnected.`); | ||
@@ -163,3 +161,2 @@ }); | ||
var _e$toString; | ||
console.error("error", e); | ||
@@ -176,3 +173,5 @@ socket.close( | ||
return wss; | ||
} // Returns websocket handler for debugger connections. | ||
} | ||
// Returns websocket handler for debugger connections. | ||
// Debugger connects to webSocketDebuggerUrl that we return as part of page description | ||
@@ -182,3 +181,2 @@ // in /json response. | ||
// websocket object to corresponding Device instance. | ||
_createDebuggerConnectionWSServer() { | ||
@@ -188,4 +186,4 @@ const wss = new WS.Server({ | ||
perMessageDeflate: false, | ||
}); // $FlowFixMe[value-as-type] | ||
}); | ||
// $FlowFixMe[value-as-type] | ||
wss.on("connection", async (socket, req) => { | ||
@@ -196,17 +194,12 @@ try { | ||
const pageId = query.page; | ||
if (deviceId == null || pageId == null) { | ||
throw new Error("Incorrect URL - must provide device and page IDs"); | ||
} | ||
const device = this._devices.get(parseInt(deviceId, 10)); | ||
if (device == null) { | ||
throw new Error("Unknown device with ID " + deviceId); | ||
} | ||
device.handleDebuggerConnection(socket, pageId); | ||
} catch (e) { | ||
var _e$toString2; | ||
console.error(e); | ||
@@ -226,3 +219,2 @@ socket.close( | ||
} | ||
module.exports = InspectorProxy; |
@@ -11,4 +11,7 @@ /** | ||
*/ | ||
"use strict"; // Page information received from the device. New page is created for | ||
"use strict"; | ||
// Page information received from the device. New page is created for | ||
// each new instance of VM and can appear when user reloads React Native | ||
// application. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
59400
814