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:
@@ -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>(<generated column>,<src url id>,<src line>,<src col>)</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>
|
||||
Reference in New Issue
Block a user