Add tool for viewing source maps.

R=johnniwinther@google.com

Review URL: https://codereview.chromium.org//280513002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@36215 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
zarah@google.com
2014-05-15 11:47:54 +00:00
parent 3324b73210
commit 113b205c5d
5 changed files with 532 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
This program serves a visualization of a JavaScript source map file generated
by dart2js or pub.
Usage: dart bin/source_map_viewer.dart <path to map file>.
The default system browser is started and pointed to the viewer if available.
@@ -0,0 +1,132 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:http_server/http_server.dart' as http_server;
import 'package:route/server.dart';
import 'package:path/path.dart';
import 'package:http_server/http_server.dart';
/*
* This program serves a visualization of a JavaScript source map file generated
* by dart2js or pub.
*
* Usage: dart source_map_viewer.dart <path to map file>.
*
* The default system browser is started and pointed to the viewer if
* available.
*/
Directory rootDir = null;
String sourceMapFile;
void main(List<String> args) {
if (args.length != 1) {
print('One argument expected; the source map file.');
exit(-1);
return;
}
File mapFile = new File(args[0]);
if (!mapFile.existsSync()) {
print('Map file not found at ${args[0]}');
exit(-2);
return;
}
sourceMapFile = basename(mapFile.path);
rootDir = mapFile.parent;
startServer(rootDir);
}
// Sends the content of the file requested in the path parameter.
void handleFile(HttpRequest request) {
String path = request.uri.queryParameters["path"];
if (path == null) {
request.response.close();
return;
}
path = rootDir.path + '/' + path;
new File(Uri.parse(path).toFilePath()).openRead()
.pipe(request.response).catchError((e) {
print("Error: $e");
request.response.close();
});
}
// Sends back the name of the source map file.
void handleSourceMapFile(HttpRequest request) {
request.response.write(sourceMapFile);
request.response.close();
}
// Starts an HttpServer rooted in [dir] with two special routes, /file and /map.
//
// /file takes a parameter [path] and serves the content of the specified file
// in path relative to [dir]
//
// /map serves the name of the map file such that its content can be requested
// with a /file request as above.
void startServer(Directory dir) {
rootDir = dir;
// Use port 0 to get an ephemeral port.
int port = 0;
HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, port).then((server) {
port = server.port;
print("Source mapping server is running on "
"'http://${server.address.address}:$port/'");
Router router = new Router(server)
..serve('/file').listen(handleFile)
..serve('/map').listen(handleSourceMapFile);
// Set up default handler. This will serve files from our 'build'
// directory. Disable jail root, as packages are local symlinks.
VirtualDirectory virDir = new http_server.VirtualDirectory(dir.path)
..jailRoot = false
..allowDirectoryListing = true;
virDir.directoryHandler = (dir, request) {
// Redirect directory requests to index.html files.
Uri indexUri = new Uri.file(dir.path).resolve('display.html');
virDir.serveFile(new File(indexUri.toFilePath()), request);
};
// Add an error page handler.
virDir.errorPageHandler = (HttpRequest request) {
print("Resource not found: ${request.uri.path}");
request.response.statusCode = HttpStatus.NOT_FOUND;
request.response.close();
};
// Serve everything not routed elsewhere through the virtual directory.
virDir.serve(router.defaultStream);
// Start the system' default browser
startBrowser('http://${server.address.address}:$port/');
});
}
startBrowser(String url) {
String command;
if (Platform.isWindows) {
command = 'cmd.exe /C start';
} else if (Platform.isMacOS) {
command = 'open';
} else {
String xdg = '/usr/bin/xdg-open';
if (new File(xdg).existsSync()) {
command = xdg;
} else {
command = '/usr/bin/google-chrome';
}
}
print('Starting browser: ${command} ${url}');
Process.run(command, ['$url']).then((ProcessResult result) {
if (result.exitCode != 0) {
print(result.stderr);
}
});
}
@@ -0,0 +1,7 @@
name: Display
description: A sample web application
dependencies:
browser: any
http_server: any
route: any
source_maps: any
@@ -0,0 +1,323 @@
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:html';
import 'dart:convert';
import 'dart:async';
import 'package:source_maps/source_maps.dart';
Element targetFileName = querySelector("#target_filename");
Element sourceFileName = querySelector("#source_filename");
DivElement generatedOutput = querySelector("#generated_output");
DivElement selectedSource = querySelector("#selected_source");
DivElement selectedOutputSpan = querySelector("#current_span");
DivElement decodedMap = querySelector("#decoded_map");
DivElement originalMap = querySelector("#original_map");
Map<TargetEntry, List<SpanElement>> targetEntryMap = {};
List<SpanElement> highlightedMapEntry = null;
List<String> target;
SingleMapping sourceMap;
void adjustDivHeightsToWindow() {
generatedOutput.style.height = "${window.innerHeight / 3 - 50}px";
selectedSource.style.height = "${window.innerHeight / 3 - 50}px";
decodedMap.style.height = "${window.innerHeight / 3 - 50}px";
originalMap.style.height = "${window.innerHeight / 3 - 50}px";
}
Future getMap() {
Completer c = new Completer();
HttpRequest httpRequest = new HttpRequest();
httpRequest
..open('GET', '/map')
..onLoadEnd.listen((_) => c.complete(httpRequest.responseText))
..send('');
return c.future;
}
Future fetchFile(String path) {
Completer c = new Completer();
HttpRequest httpRequest = new HttpRequest();
sourceFileName.text = path;
httpRequest
..open('GET', path)
..onLoadEnd.listen((_) => c.complete(httpRequest.responseText))
..send('');
return c.future;
}
displaySource(String filename, List<String> source, TargetEntry entry) {
int line = entry.sourceLine;
int column = entry.sourceColumn;
int nameId = entry.sourceNameId;
String id = nameId == null ? null : sourceMap.names[nameId];
selectedSource.children.clear();
SpanElement marker = new SpanElement()
..className = "marker"
..appendText("*");
for (int pos = 0; pos < source.length; pos++) {
String l = source[pos];
if (pos != line) {
selectedSource.children.add(l.isEmpty ? new BRElement() : new DivElement()
..appendText(l));
} else {
selectedSource.children.add(new DivElement()
..appendText(l.substring(0, column))
..children.add(marker)
..appendText(l.substring(column)));
}
}
sourceFileName.text = filename;
marker.scrollIntoView();
}
void highlightSelectedSpan(TargetEntry entry, TargetLineEntry lineEntry) {
selectedOutputSpan.children.clear();
String spanEndCol;
TargetEntry spanEnd;
bool nextEntryIsSpanEnd = false;
for (TargetEntry e in lineEntry.entries) {
if (nextEntryIsSpanEnd) {
spanEnd = e;
break;
}
if (e == entry) {
nextEntryIsSpanEnd = true;
}
}
if (spanEnd == null) {
spanEndCol = '${target[lineEntry.line].length} (EOL).';
} else {
spanEndCol = '${spanEnd.column}.';
}
String targetSpan =
'Target: Line ${lineEntry.line} Col. ${entry.column} - $spanEndCol';
if (entry.sourceUrlId == null) {
targetSpan += ' Source: unknown';
selectedOutputSpan.children.add(getTextElement(targetSpan));
return;
}
String source = sourceMap.urls[entry.sourceUrlId];
String sourceName = source.substring(source.lastIndexOf('/') + 1);
String sourcePoint =
'Source: Line ${entry.sourceLine} Col. ${entry.sourceColumn}';
sourcePoint +=
entry.sourceNameId == null ? ''
: ' (${sourceMap.names[entry.sourceNameId]})';
sourcePoint += ' in $sourceName';
selectedOutputSpan.children.add(getTextElement(targetSpan));
selectedOutputSpan.children.add(new BRElement());
selectedOutputSpan.children.add(getTextElement(sourcePoint));
if (highlightedMapEntry != null) {
highlightedMapEntry[0].style.background = 'white';
highlightedMapEntry[1].style.background = 'white';
}
String highlightColor = "#99ff99";
highlightedMapEntry = targetEntryMap[entry];
highlightedMapEntry[0]
..scrollIntoView()
..style.backgroundColor = highlightColor;
highlightedMapEntry[1]
..scrollIntoView()
..style.backgroundColor = highlightColor;
highlightedMapEntry[1].onMouseOver.listen((e) {
selectedOutputSpan.style.zIndex = "2";
selectedOutputSpan.style.visibility = "visible";
selectedOutputSpan.style.top = "${decodedMap.offsetTo(document.body).y +
decodedMap.clientHeight - 20}px";
selectedOutputSpan.style.left = "${decodedMap.offsetTo(document.body).x}px";
selectedOutputSpan.style.width= "${decodedMap.clientWidth}px";
});
highlightedMapEntry[1].onMouseOut.listen( (e) {
selectedOutputSpan.style.visibility = "hidden";
});
adjustDivHeightsToWindow();
}
void loadSource(TargetEntry entry) {
if (entry.sourceUrlId == null) {
return;
}
String source = sourceMap.urls[entry.sourceUrlId];
fetchFile(new Uri(path: "/file",
queryParameters: {"path": source}).toString()).then((text)
=> displaySource(source, text.split("\n"), entry));
selectedSource.text = "loading";
}
SpanElement createSpan(String content, TargetEntry entry,
TargetLineEntry lineEntry) {
return new SpanElement()
..addEventListener('click', (e) {
loadSource(entry);
highlightSelectedSpan(entry, lineEntry);
}, false)
..className = "range${entry.sourceUrlId % 4}"
..appendText(content);
}
Element getLineNumberElement(int line) {
SpanElement result = new SpanElement();
result.style.fontFamily = "Courier";
result.style.fontSize = "10pt";
result.appendText("${line} ");
return result;
}
Element getTextElement(String text) {
SpanElement result = new SpanElement();
result.text = text;
return result;
}
addTargetLine(int lineNumber, String content, TargetLineEntry lineEntry) {
if (content.isEmpty) {
generatedOutput.children.add(new DivElement()
..children.add(getLineNumberElement(lineNumber)));
return;
}
if (lineEntry == null) {
generatedOutput.children.add(new DivElement()
..children.add(getLineNumberElement(lineNumber))
..children.add(getTextElement(content)));
return;
}
DivElement div = new DivElement();
div.children.add(getLineNumberElement(lineNumber));
int pos = 0;
TargetEntry previous = null;
for (TargetEntry next in lineEntry.entries) {
if (previous == null) {
if (pos < next.column) {
div.appendText(content.substring(pos, next.column));
}
if (content.length == next.column) {
div.children.add(createSpan(" ", next, lineEntry));
}
} else {
if (next.column <= content.length) {
String token = content.substring(pos, next.column);
div.children.add(createSpan(token, previous, lineEntry));
}
if (content.length == next.column) {
div.children.add(createSpan(" ", next, lineEntry));
}
}
pos = next.column;
previous = next;
}
String token = content.substring(pos);
if (previous == null) {
div.appendText(token);
} else {
div..children.add(createSpan(token, previous, lineEntry));
}
generatedOutput.children.add(div);
}
// Display the target source in the HTML.
void displayTargetSource() {
List<TargetLineEntry> targetLines = sourceMap.lines;
int linesIndex = 0;
for (int line = 0; line < target.length; line++) {
TargetLineEntry entry = null;
if (linesIndex < targetLines.length
&& targetLines[linesIndex].line == line) {
entry = targetLines[linesIndex];
linesIndex++;
}
if (entry != null) {
addTargetLine(line, target[line], entry);
} else {
addTargetLine(line, target[line], null);
}
}
}
String getMappedData(String mapFileContent) {
// Source map contains mapping information in this format:
// "mappings": "A;A,yC;"
List<String> mapEntry = mapFileContent.split('mappings');
return mapEntry[mapEntry.length-1].split('"')[2];
}
SpanElement createMapSpan(String segment) {
return new SpanElement()..text = segment;
}
SpanElement createDecodedMapSpan(TargetEntry entry) {
return new SpanElement()..text = '(${entry.column}, ${entry.sourceUrlId},'
' ${entry.sourceLine},'
' ${entry.sourceColumn})';
}
displayMap(String mapFileContent) {
String mappedData = getMappedData(mapFileContent);
int sourceMapLine = 0;
for (String group in mappedData.split(';')) {
if (group.length == 0) continue;
List<String> segments = [];
if (!group.contains(',')) {
segments.add(group);
} else {
segments = group.split(',');
}
TargetLineEntry targetLineEntry = sourceMap.lines[sourceMapLine];
decodedMap.children.add(getLineNumberElement(targetLineEntry.line));
originalMap.children.add(getLineNumberElement(targetLineEntry.line));
bool first = true;
int entryNumber = 0;
for (String segment in segments) {
TargetEntry entry = targetLineEntry.entries[entryNumber];
SpanElement orignalMapSpan = createMapSpan(segment);
SpanElement decodedMapSpan = createDecodedMapSpan(entry);
if (first) {
first = false;
} else {
originalMap.children.add(getTextElement(', '));
decodedMap.children.add(getTextElement(', '));
}
originalMap.children.add(orignalMapSpan);
decodedMap.children.add(decodedMapSpan);
++entryNumber;
targetEntryMap.putIfAbsent(entry, () => [orignalMapSpan, decodedMapSpan]);
}
originalMap.children.add(new BRElement());
decodedMap.children.add(new BRElement());
++sourceMapLine;
}
}
void main() {
Future load(String q) => fetchFile(new Uri(path: "/file", queryParameters: {
"path": q
}).toString());
getMap().then((mapFileName) {
load(mapFileName).then((mapFileContent) {
sourceMap = new SingleMapping.fromJson(JSON.decode(mapFileContent));
displayMap(mapFileContent);
targetFileName.text = sourceMap.targetUrl;
load(targetFileName.text).then((targetFileContent) {
target = targetFileContent.split('\n');
displayTargetSource();
adjustDivHeightsToWindow();
});
});
});
sourceFileName.text = "<source not selected>";
}
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Display</title>
</head>
<style>
span.range0 {color:green;background-color:#DA81F5}
span.range1 {color:green;background-color:#81F7D8}
span.range2 {color:green;background-color:#BEF781}
span.range3 {color:green;background-color:#F79F81}
span.marker {
color:green;
background-color:red;
white-space: pre
}
#group{
width:100%;
height:100%;
}
span.source pre {
white-space: pre;
font-family: monospace
}
div#generated_output div{
white-space: pre;
font-family: monospace
}
div#selected_source div{
white-space: pre;
font-family: monospace
}
</style>
<body>
<h1>Display</h1>
<div id="group">
<table style="width:100%;">
<tr><td>
<h2>Generated Output</h2>
<div id="target_filename"></div>
<div id="generated_output" style="border:2px solid;overflow:scroll;width:550px;height:50px;"></div>
</td><td>
<h2>Selected Source Code</h2>
<div id="source_filename"></div>
<div id="selected_source" style="border:2px solid;overflow:scroll;width:550px;height:50px;"></div>
</td><td>
<div id="current_span" style="background-color: #99ff99;visibility:hidden;border:green 1px dashed;overflow:scroll;width:250px;height:50px;font-size:12pt;position:absolute;"></div>
</td>
<tr><td>
<h2>Decoded Map</h2>
<small><i>(&lt;generated column&gt;,&lt;src url id&gt;,&lt;src line&gt;,&lt;src col&gt;)</i></small>
<div id="decoded_map" style="border: 2px solid;overflow:scroll;width:550px;height:50px;"></div>
</td><td>
<h2>Original Map</h2>
<small><i>The encoded mapping data.</i></small>
<div id="original_map" style="border: 2px solid;overflow:scroll;width:550px;height:50px;"></div>
</td>
</tr>
</table></div>
<script type="application/dart" src="display.dart"></script>
<script src="packages/browser/dart.js"></script>
</body>
</html>