diff --git a/teavm-cli/src/main/java/org/teavm/cli/TeaVMRunner.java b/teavm-cli/src/main/java/org/teavm/cli/TeaVMRunner.java index 094757564..b8b205a3d 100644 --- a/teavm-cli/src/main/java/org/teavm/cli/TeaVMRunner.java +++ b/teavm-cli/src/main/java/org/teavm/cli/TeaVMRunner.java @@ -62,6 +62,10 @@ public class TeaVMRunner { .withDescription("Generate debug information") .withLongOpt("debug") .create('D')); + options.addOption(OptionBuilder + .withDescription("Generate source maps") + .withLongOpt("sourcemaps") + .create()); options.addOption(OptionBuilder .withArgName("number") .hasArg() @@ -126,6 +130,9 @@ public class TeaVMRunner { } if (commandLine.hasOption('D')) { tool.setDebugInformation(new File(tool.getTargetDirectory(), tool.getTargetFileName() + ".teavmdbg")); + if (commandLine.hasOption("sourcemaps")) { + tool.setSourceMapsFileGenerated(true); + } } args = commandLine.getArgs(); if (args.length > 1) { diff --git a/teavm-core/src/main/java/org/teavm/debugging/DebugInformation.java b/teavm-core/src/main/java/org/teavm/debugging/DebugInformation.java index aef99bbb6..e6c3d61e5 100644 --- a/teavm-core/src/main/java/org/teavm/debugging/DebugInformation.java +++ b/teavm-core/src/main/java/org/teavm/debugging/DebugInformation.java @@ -15,10 +15,7 @@ */ package org.teavm.debugging; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.util.*; import org.teavm.common.IntegerArray; import org.teavm.model.MethodDescriptor; @@ -296,6 +293,10 @@ public class DebugInformation { writer.write(this); } + public void writeAsSourceMaps(Writer output, String sourceFile) throws IOException { + new SourceMapsWriter(output).write(sourceFile, this); + } + public static DebugInformation read(InputStream input) throws IOException { DebugInformationReader reader = new DebugInformationReader(input); return reader.read(); diff --git a/teavm-core/src/main/java/org/teavm/debugging/SourceMapsWriter.java b/teavm-core/src/main/java/org/teavm/debugging/SourceMapsWriter.java new file mode 100644 index 000000000..0ee05ba35 --- /dev/null +++ b/teavm-core/src/main/java/org/teavm/debugging/SourceMapsWriter.java @@ -0,0 +1,159 @@ +/* + * Copyright 2014 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.debugging; + +import java.io.IOException; +import java.io.Writer; + +/** + * + * @author Alexey Andreev + */ +class SourceMapsWriter { + private static final String BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + private Writer output; + private int lastLine; + private int lastColumn; + private int sourceLine; + private int lastSourceLine; + private int sourceFile; + private int lastSourceFile; + private boolean first; + + public SourceMapsWriter(Writer output) { + this.output = output; + } + + public void write(String generatedFile, DebugInformation debugInfo) throws IOException { + output.write("{\"version\":3"); + output.write(",\"file\":\""); + writeEscapedString(generatedFile); + output.write("\""); + output.write(",\"sourceRoot\":\"\""); + output.write(",\"sources\":["); + for (int i = 0; i < debugInfo.fileNames.length; ++i) { + if (i > 0) { + output.write(','); + } + output.write("\""); + writeEscapedString(debugInfo.fileNames[i]); + output.write("\""); + } + output.write("]"); + output.write(",\"names\":[]"); + output.write(",\"mappings\":\""); + first = true; + lastLine = 0; + lastColumn = 0; + sourceLine = -1; + sourceFile = -1; + lastSourceFile = 0; + lastSourceLine = 0; + int i = 0; + int j = 0; + while (i < debugInfo.lineMapping.lines.length && j < debugInfo.fileMapping.lines.length) { + GeneratedLocation a = debugInfo.lineMapping.key(i); + GeneratedLocation b = debugInfo.fileMapping.key(j); + int cmp = a.compareTo(b); + if (cmp < 0) { + writeSegment(a, sourceFile, debugInfo.lineMapping.values[i++]); + } else if (cmp > 0) { + writeSegment(b, debugInfo.fileMapping.values[j++], sourceLine); + } else { + writeSegment(a, debugInfo.fileMapping.values[j++], debugInfo.lineMapping.values[i++] - 1); + } + } + while (i < debugInfo.lineMapping.lines.length) { + GeneratedLocation a = debugInfo.lineMapping.key(i); + writeSegment(a, sourceFile, debugInfo.lineMapping.values[i++]); + } + while (j < debugInfo.fileMapping.lines.length) { + GeneratedLocation b = debugInfo.fileMapping.key(j); + writeSegment(b, debugInfo.fileMapping.values[j++], sourceLine); + } + output.write("\"}"); + } + + private void writeSegment(GeneratedLocation loc, int sourceFile, int sourceLine) throws IOException { + while (loc.getLine() > lastLine) { + output.write(';'); + ++lastLine; + first = true; + lastColumn = 0; + } + if (!first) { + output.write(','); + } + writeVLQ(loc.getColumn() - lastColumn); + if (sourceFile >= 0 && sourceLine >= 0) { + writeVLQ(sourceFile - lastSourceFile); + writeVLQ(sourceLine - lastSourceLine); + writeVLQ(0); + lastSourceFile = sourceFile; + lastSourceLine = sourceLine; + } + lastColumn = loc.getColumn(); + this.sourceFile = sourceFile; + this.sourceLine = sourceLine; + first = false; + } + + private void writeEscapedString(String str) throws IOException { + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + switch (c) { + case '\n': + output.write("\\n"); + break; + case '\r': + output.write("\\r"); + break; + case '\t': + output.write("\\t"); + break; + case '\b': + output.write("\\b"); + break; + case '\\': + output.write("\\\\"); + break; + case '"': + output.write("\\\""); + break; + default: + output.write(c); + break; + } + } + } + + private void writeVLQ(int number) throws IOException { + if (number < 0) { + number = ((-number) << 1) | 1; + } else { + number = number << 1; + } + do { + int digit = number & 0x1F; + int next = number >>> 5; + if (next != 0) { + digit |= 0x20; + } + output.write(BASE64_CHARS.charAt(digit)); + number = next; + } while (number != 0); + } +} diff --git a/teavm-core/src/main/java/org/teavm/tooling/TeaVMTool.java b/teavm-core/src/main/java/org/teavm/tooling/TeaVMTool.java index 5da670eaa..96eced894 100644 --- a/teavm-core/src/main/java/org/teavm/tooling/TeaVMTool.java +++ b/teavm-core/src/main/java/org/teavm/tooling/TeaVMTool.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Properties; import org.apache.commons.io.IOUtils; import org.teavm.common.ThreadPoolFiniteExecutor; +import org.teavm.debugging.DebugInformation; import org.teavm.debugging.DebugInformationBuilder; import org.teavm.javascript.RenderingContext; import org.teavm.model.ClassHolderTransformer; @@ -45,6 +46,8 @@ public class TeaVMTool { private boolean mainPageIncluded; private boolean bytecodeLogging; private File debugInformation; + private String sourceMapsFileName; + private boolean sourceMapsFileGenerated; private int numThreads = 1; private List transformers = new ArrayList<>(); private List classAliases = new ArrayList<>(); @@ -124,6 +127,22 @@ public class TeaVMTool { this.numThreads = numThreads; } + public String getSourceMapsFileName() { + return sourceMapsFileName; + } + + public void setSourceMapsFileName(String sourceMapsFileName) { + this.sourceMapsFileName = sourceMapsFileName; + } + + public boolean isSourceMapsFileGenerated() { + return sourceMapsFileGenerated; + } + + public void setSourceMapsFileGenerated(boolean sourceMapsFileGenerated) { + this.sourceMapsFileGenerated = sourceMapsFileGenerated; + } + public Properties getProperties() { return properties; } @@ -218,10 +237,23 @@ public class TeaVMTool { vm.checkForMissingItems(); log.info("JavaScript file successfully built"); if (debugInformation != null) { + DebugInformation debugInfo = debugEmitter.getDebugInformation(); try (OutputStream debugInfoOut = new FileOutputStream(debugInformation)) { - debugEmitter.getDebugInformation().write(debugInfoOut); + debugInfo.write(debugInfoOut); } log.info("Debug information successfully written"); + if (sourceMapsFileGenerated) { + String sourceMapsFileName = this.sourceMapsFileName; + if (sourceMapsFileName == null) { + sourceMapsFileName = targetFileName + ".map"; + } + writer.append("\n//# sourceMappingURL=").append(sourceMapsFileName); + try (Writer sourceMapsOut = new OutputStreamWriter(new FileOutputStream( + new File(targetDirectory, sourceMapsFileName)), "UTF-8")) { + debugInfo.writeAsSourceMaps(sourceMapsOut, targetFileName); + } + log.info("Source maps successfully written"); + } } } if (runtime == RuntimeCopyOperation.SEPARATE) {