[ReactNative] Bring Chrome debugger to OSS. Part 2

This commit is contained in:
Alex Kotliarskyi 2015-03-19 12:10:41 -07:00
parent da2b122e21
commit 8dea55618d
5 changed files with 229 additions and 2 deletions

View File

@ -21,7 +21,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
- (instancetype)init - (instancetype)init
{ {
return [self initWithURL:[NSURL URLWithString:@"http://localhost:8081"]]; return [self initWithURL:[NSURL URLWithString:@"http://localhost:8081/debugger-proxy"]];
} }
- (instancetype)initWithURL:(NSURL *)url - (instancetype)initWithURL:(NSURL *)url
@ -35,6 +35,10 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply);
_injectedObjects = [NSMutableDictionary dictionary]; _injectedObjects = [NSMutableDictionary dictionary];
[_socket setDelegateOperationQueue:_jsQueue]; [_socket setDelegateOperationQueue:_jsQueue];
NSURL *startDevToolsURL = [NSURL URLWithString:@"/launch-chrome-devtools" relativeToURL:url];
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:startDevToolsURL] delegate:nil];
if (![self connectToProxy]) { if (![self connectToProxy]) {
RCTLogError(@"Connection to %@ timed out. Are you running node proxy?", url); RCTLogError(@"Connection to %@ timed out. Are you running node proxy?", url);
[self invalidate]; [self invalidate];

112
packager/debugger.html Normal file
View File

@ -0,0 +1,112 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<!-- Fake favicon, to avoid extra request to server -->
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<title>React Native Debugger</title>
<script>
(function() {
var sessionID = window.localStorage.getItem('sessionID');
window.localStorage.removeItem('sessionID');
window.onbeforeunload = function() {
if (sessionID) {
return 'If you reload this page, it is going to break the debugging session. ' +
'You should ⌘+R in the iOS simulator to reload.';
}
};
function setStatus(status) {
document.getElementById('status').innerHTML = status;
}
var messageHandlers = {
// This method is a bit hacky. Catalyst asks for a new clean JS runtime.
// The easiest way to do this is to reload this page. That also means that
// web socket connection will be lost. To send reply back we need to remember
// message id
'prepareJSRuntime': function(message) {
window.onbeforeunload = undefined;
window.localStorage.setItem('sessionID', message.id);
window.location.reload();
},
'executeApplicationScript:sourceURL:onComplete:': function(message, sendReply) {
for (var key in message.inject) {
window[key] = JSON.parse(message.inject[key]);
}
loadScript(message.url, sendReply.bind(null, null));
},
'executeJSCall:method:arguments:callback:': function(message, sendReply) {
var returnValue = [[], [], [], [], []];
try {
if (window && window.require) {
returnValue = window.require(message.moduleName)[message.moduleMethod].apply(null, message.arguments);
}
} finally {
sendReply(JSON.stringify(returnValue));
}
}
}
var ws = new WebSocket('ws://' + window.location.host + '/debugger-proxy');
ws.onopen = function() {
if (sessionID) {
setStatus('Debugger session #' + sessionID + ' active');
ws.send(JSON.stringify({replyID: parseInt(sessionID, 10)}));
} else {
setStatus('Waiting for simulator');
}
}
ws.onmessage = function(message) {
var object = JSON.parse(message.data);
var sendReply = function(result) {
ws.send(JSON.stringify({replyID: object.id, result: result}));
}
var handler = messageHandlers[object.method];
if (handler) {
handler(object, sendReply);
} else {
console.warn('Unknown method: ' + object.method);
}
}
ws.onclose = function() {
setStatus('Disconnected from proxy. Is node server running?');
}
function loadScript(src, callback) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = src;
script.onload = callback;
document.head.appendChild(script);
}
})();
</script>
<style type="text/css">
.shortcut {
font-family: monospace;
color: #eee;
background-color: #333;
padding: 4px;
border-radius: 4px;
letter-spacing: 3px;
}
body {
font-size: large;
}
</style>
</head>
<body>
<p>
React Native JS code runs inside this Chrome tab
</p>
<p>Press <span class="shortcut">⌘⌥J</span> to open Developer Tools. Enable <a href="http://stackoverflow.com/a/17324511/232122" target="_blank">Pause On Caught Exceptions</a> for a better debugging experience.</p>
<p>Status: <span id="status">Loading</span></p>
</body>
</html>

View File

@ -0,0 +1,41 @@
#!/usr/bin/env osascript
on run argv
set theURL to item 1 of argv
tell application "Google Chrome"
activate
if (count every window) = 0 then
make new window
end if
-- Find a tab currently running the debugger
set found to false
set theTabIndex to -1
repeat with theWindow in every window
set theTabIndex to 0
repeat with theTab in every tab of theWindow
set theTabIndex to theTabIndex + 1
if theTab's URL is theURL then
set found to true
exit repeat
end if
end repeat
if found then
exit repeat
end if
end repeat
if found then
set index of theWindow to 1
set theWindow's active tab index to theTabIndex
else
tell window 1
make new tab with properties {URL:theURL}
end tell
end if
end tell
end run

View File

@ -16,12 +16,14 @@ if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) {
process.exit(); process.exit();
} }
var exec = require('child_process').exec;
var ReactPackager = require('./react-packager'); var ReactPackager = require('./react-packager');
var blacklist = require('./blacklist.js'); var blacklist = require('./blacklist.js');
var connect = require('connect'); var connect = require('connect');
var http = require('http'); var http = require('http');
var launchEditor = require('./launchEditor.js'); var launchEditor = require('./launchEditor.js');
var parseCommandLine = require('./parseCommandLine.js'); var parseCommandLine = require('./parseCommandLine.js');
var webSocketProxy = require('./webSocketProxy.js');
var options = parseCommandLine([{ var options = parseCommandLine([{
command: 'port', command: 'port',
@ -68,10 +70,12 @@ process.on('uncaughtException', function(e) {
'any existing instances that are already running.\n\n'); 'any existing instances that are already running.\n\n');
}); });
runServer(options, function() { var server = runServer(options, function() {
console.log('\nReact packager ready.\n'); console.log('\nReact packager ready.\n');
}); });
webSocketProxy.attachToServer(server, '/debugger-proxy');
function loadRawBody(req, res, next) { function loadRawBody(req, res, next) {
req.rawBody = ''; req.rawBody = '';
req.setEncoding('utf8'); req.setEncoding('utf8');
@ -95,6 +99,30 @@ function openStackFrameInEditor(req, res, next) {
} }
} }
function getDevToolsLauncher(options) {
return function(req, res, next) {
if (req.url === '/debugger-ui') {
var debuggerPath = path.join(__dirname, 'debugger.html');
res.writeHead(200, {'Content-Type': 'text/html'});
fs.createReadStream(debuggerPath).pipe(res);
} else if (req.url === '/launch-chrome-devtools') {
var debuggerURL = 'http://localhost:' + options.port + '/debugger-ui';
var script = 'launchChromeDevTools.applescript';
console.log('Launching Dev Tools...');
exec(path.join(__dirname, script) + ' ' + debuggerURL, function(err, stdout, stderr) {
if (err) {
console.log('Failed to run ' + script, err);
}
console.log(stdout);
console.warn(stderr);
});
res.end('OK');
} else {
next();
}
};
}
function getAppMiddleware(options) { function getAppMiddleware(options) {
return ReactPackager.middleware({ return ReactPackager.middleware({
projectRoots: options.projectRoots, projectRoots: options.projectRoots,
@ -112,6 +140,7 @@ function runServer(
var app = connect() var app = connect()
.use(loadRawBody) .use(loadRawBody)
.use(openStackFrameInEditor) .use(openStackFrameInEditor)
.use(getDevToolsLauncher(options))
.use(getAppMiddleware(options)); .use(getAppMiddleware(options));
options.projectRoots.forEach(function(root) { options.projectRoots.forEach(function(root) {

View File

@ -0,0 +1,41 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*/
'use strict';
var WebSocketServer = require('ws').Server;
function attachToServer(server, path) {
var wss = new WebSocketServer({
server: server,
path: path
});
var clients = [];
wss.on('connection', function(ws) {
clients.push(ws);
var allClientsExcept = function(ws) {
return clients.filter(function(cn) { return cn !== ws; });
};
ws.onerror = function() {
clients = allClientsExcept(ws);
};
ws.onclose = function() {
clients = allClientsExcept(ws);
};
ws.on('message', function(message) {
allClientsExcept(ws).forEach(function(cn) {
cn.send(message);
});
});
});
}
module.exports = {
attachToServer: attachToServer
};