From 113b205c5dcfb4f3da35b24a1e072bc71ddfc5bc Mon Sep 17 00:00:00 2001 From: "zarah@google.com" Date: Thu, 15 May 2014 11:47:54 +0000 Subject: [PATCH] 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 --- tools/dart2js/sourceMapViewer/README.TXT | 7 + .../bin/source_map_viewer.dart | 132 +++++++ tools/dart2js/sourceMapViewer/pubspec.yaml | 7 + .../dart2js/sourceMapViewer/web/display.dart | 323 ++++++++++++++++++ .../dart2js/sourceMapViewer/web/display.html | 63 ++++ 5 files changed, 532 insertions(+) create mode 100644 tools/dart2js/sourceMapViewer/README.TXT create mode 100644 tools/dart2js/sourceMapViewer/bin/source_map_viewer.dart create mode 100644 tools/dart2js/sourceMapViewer/pubspec.yaml create mode 100644 tools/dart2js/sourceMapViewer/web/display.dart create mode 100644 tools/dart2js/sourceMapViewer/web/display.html diff --git a/tools/dart2js/sourceMapViewer/README.TXT b/tools/dart2js/sourceMapViewer/README.TXT new file mode 100644 index 00000000000..aa8cab51799 --- /dev/null +++ b/tools/dart2js/sourceMapViewer/README.TXT @@ -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 . + +The default system browser is started and pointed to the viewer if available. + diff --git a/tools/dart2js/sourceMapViewer/bin/source_map_viewer.dart b/tools/dart2js/sourceMapViewer/bin/source_map_viewer.dart new file mode 100644 index 00000000000..8981862387e --- /dev/null +++ b/tools/dart2js/sourceMapViewer/bin/source_map_viewer.dart @@ -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 . + * + * The default system browser is started and pointed to the viewer if + * available. + */ + +Directory rootDir = null; +String sourceMapFile; + +void main(List 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); + } + }); +} \ No newline at end of file diff --git a/tools/dart2js/sourceMapViewer/pubspec.yaml b/tools/dart2js/sourceMapViewer/pubspec.yaml new file mode 100644 index 00000000000..4ab317d6001 --- /dev/null +++ b/tools/dart2js/sourceMapViewer/pubspec.yaml @@ -0,0 +1,7 @@ +name: Display +description: A sample web application +dependencies: + browser: any + http_server: any + route: any + source_maps: any diff --git a/tools/dart2js/sourceMapViewer/web/display.dart b/tools/dart2js/sourceMapViewer/web/display.dart new file mode 100644 index 00000000000..a6d02bec248 --- /dev/null +++ b/tools/dart2js/sourceMapViewer/web/display.dart @@ -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> targetEntryMap = {}; +List highlightedMapEntry = null; +List 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 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 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 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 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 = ""; +} \ No newline at end of file diff --git a/tools/dart2js/sourceMapViewer/web/display.html b/tools/dart2js/sourceMapViewer/web/display.html new file mode 100644 index 00000000000..f6d675869a0 --- /dev/null +++ b/tools/dart2js/sourceMapViewer/web/display.html @@ -0,0 +1,63 @@ + + + + + Display + + + +

Display

+
+ + + + +
+

Generated Output

+
+
+
+

Selected Source Code

+
+
+
+ +
+

Decoded Map

+ (<generated column>,<src url id>,<src line>,<src col>) +
+
+

Original Map

+ The encoded mapping data. +
+
+ + + + \ No newline at end of file