Skip to content

Commit a3a8436

Browse files
committed
Rough draft of bundling sourcemaps and original sources
This will cost extra CPU time, but save on IO time and disk space, which is likely a net win for most builds. This is a first draft, which appears to work as expected (sourcemaps function correctly, vastly smaller output in BUNDLE_JAR), but doesn't yet fully reduce the number of copies we can make, and doesn't avoid copying sourcemap bundles (in cases where js bundles are not copied). There may also be caching issues, where sourcemaps are incorrect not generated or are regenerated.
1 parent 320e5ba commit a3a8436

File tree

2 files changed

+110
-19
lines changed

2 files changed

+110
-19
lines changed

j2cl-tasks/src/main/java/com/vertispan/j2cl/build/provided/BundleJarTask.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@
55
import com.google.gson.GsonBuilder;
66
import com.google.javascript.jscomp.deps.ClosureBundler;
77
import com.vertispan.j2cl.build.task.*;
8-
import com.vertispan.j2cl.tools.Closure;
9-
import org.apache.commons.io.FileUtils;
108

119
import java.io.File;
1210
import java.io.IOException;
1311
import java.io.UncheckedIOException;
14-
import java.nio.file.FileSystems;
1512
import java.nio.file.Files;
1613
import java.nio.file.Path;
1714
import java.nio.file.PathMatcher;
@@ -53,7 +50,7 @@ public Task resolve(Project project, Config config) {
5350
.map(inputs(OutputTypes.BUNDLED_JS)),
5451
Stream.of(input(project, OutputTypes.BUNDLED_JS))
5552
)
56-
.map(i -> i.filter(BUNDLE_JS))
53+
.map(i -> i.filter(BUNDLE_JS, withSuffix(".bundle.js.map")))
5754
.collect(Collectors.toUnmodifiableList());
5855

5956
// Sort the projects, to try to include them in order. We can't be sure that all project
@@ -113,19 +110,20 @@ public void finish(TaskContext taskContext) throws IOException {
113110
Files.copy(bundle.getAbsolutePath(), targetFile, StandardCopyOption.REPLACE_EXISTING);
114111
}
115112

116-
File destSourcesDir = outputDir.toPath().resolve(Closure.SOURCES_DIRECTORY_NAME).toFile();
117-
destSourcesDir.mkdirs();
118-
for (Path dir : jsSources.stream().map(Input::getParentPaths).flatMap(Collection::stream).map(p -> p.resolve(Closure
119-
.SOURCES_DIRECTORY_NAME)).collect(Collectors.toSet())) {
120-
FileUtils.copyDirectory(dir.toFile(), destSourcesDir);
121-
}
113+
// File destSourcesDir = outputDir.toPath().resolve(Closure.SOURCES_DIRECTORY_NAME).toFile();
114+
// destSourcesDir.mkdirs();
115+
// for (Path dir : jsSources.stream().map(Input::getParentPaths).flatMap(Collection::stream).map(p -> p.resolve(Closure
116+
// .SOURCES_DIRECTORY_NAME)).collect(Collectors.toSet())) {
117+
// FileUtils.copyDirectory(dir.toFile(), destSourcesDir);
118+
// }
122119

123120
try {
124121
Gson gson = new GsonBuilder().setPrettyPrinting().create();
125122
String scriptsArray = gson.toJson(sourceOrder.stream()
126123
.flatMap(i -> i.getFilesAndHashes().stream())
127124
.map(CachedPath::getSourcePath)
128125
.map(Path::toString)
126+
.filter(s -> s.endsWith(".js"))
129127
.collect(Collectors.toUnmodifiableList())
130128
);
131129
// unconditionally set this to false, so that our dependency order works, since we're always in BUNDLE now

j2cl-tasks/src/main/java/com/vertispan/j2cl/build/provided/ClosureBundleTask.java

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.google.common.collect.ImmutableList;
55
import com.google.common.collect.ImmutableMap;
66
import com.google.common.reflect.TypeToken;
7+
import com.google.debugging.sourcemap.SourceMapConsumerV3;
8+
import com.google.debugging.sourcemap.SourceMapGeneratorV3;
79
import com.google.gson.Gson;
810
import com.google.gson.GsonBuilder;
911
import com.google.javascript.jscomp.Compiler;
@@ -32,11 +34,13 @@
3234
import java.io.BufferedReader;
3335
import java.io.BufferedWriter;
3436
import java.io.File;
37+
import java.io.FilterWriter;
3538
import java.io.IOException;
3639
import java.io.InputStream;
3740
import java.io.InputStreamReader;
3841
import java.io.OutputStream;
3942
import java.io.OutputStreamWriter;
43+
import java.io.Writer;
4044
import java.lang.reflect.Type;
4145
import java.nio.charset.StandardCharsets;
4246
import java.nio.file.Files;
@@ -121,6 +125,7 @@ public Task resolve(Project project, Config config) {
121125
List<DependencyInfoAndSource> dependencyInfos = new ArrayList<>();
122126
Compiler jsCompiler = new Compiler(System.err);//TODO before merge, write this to the log
123127

128+
Path sourcesPath = context.outputPath().resolve(Closure.SOURCES_DIRECTORY_NAME);
124129
if (incrementalEnabled && context.lastSuccessfulOutput().isPresent()) {
125130
// collect any dep info from disk for existing files
126131
final Map<String, DependencyInfoAndSource> depInfoMap;
@@ -145,7 +150,7 @@ public Task resolve(Project project, Config config) {
145150
} else {
146151
// ADD or MODIFY
147152
CompilerInput input = new CompilerInput(SourceFile.builder()
148-
.withPath(context.outputPath().resolve(Closure.SOURCES_DIRECTORY_NAME).resolve(change.getSourcePath()))
153+
.withPath(sourcesPath.resolve(change.getSourcePath()))
149154
.withOriginalPath(change.getSourcePath().toString())
150155
.build());
151156
input.setCompiler(jsCompiler);
@@ -166,7 +171,7 @@ public Task resolve(Project project, Config config) {
166171
for (Input jsInput : js) {
167172
for (CachedPath path : jsInput.getFilesAndHashes()) {
168173
CompilerInput input = new CompilerInput(SourceFile.builder()
169-
.withPath(context.outputPath().resolve(Closure.SOURCES_DIRECTORY_NAME).resolve(path.getSourcePath()))
174+
.withPath(sourcesPath.resolve(path.getSourcePath()))
170175
.withOriginalPath(path.getSourcePath().toString())
171176
.build());
172177
input.setCompiler(jsCompiler);
@@ -182,6 +187,8 @@ public Task resolve(Project project, Config config) {
182187

183188
// TODO optional/stretch-goal find first change in the list, so we can keep old prefix of bundle output
184189

190+
SourceMapGeneratorV3 sourceMapGenerator = new SourceMapGeneratorV3();
191+
185192
// rebundle all (optional: remaining) files using this already handled sort
186193
ClosureBundler bundler = new ClosureBundler(Transpiler.NULL, new BaseTranspiler(
187194
new BaseTranspiler.CompilerSupplier(
@@ -195,27 +202,52 @@ public Task resolve(Project project, Config config) {
195202
ImmutableMap.of()
196203
),
197204
""
198-
)).useEval(true);
205+
)).useEval(false);
206+
207+
String sourcemapOutFileName = fileNameKey + ".bundle.js.map";
199208

200-
try (OutputStream outputStream = Files.newOutputStream(Paths.get(outputFile));
201-
BufferedWriter bundleOut = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
209+
try (OutputStream outputStream = Files.newOutputStream(outputFilePath);
210+
BufferedWriter bundleOut = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
211+
LineCountingWriter writer = new LineCountingWriter(bundleOut)) {
202212
for (DependencyInfoAndSource info : sorter.getSortedList()) {
203213
String code = info.getSource();
204214
String name = info.getName();
215+
String sourcemapContents = info.loadSourcemap(sourcesPath);
205216

206217
//TODO do we actually need this?
207218
if (Compiler.isFillFileName(name) && code.isEmpty()) {
208219
continue;
209220
}
210221

211-
// append this file and a comment where it came from
212-
bundleOut.append("//").append(name).append("\n");
213-
bundler.withPath(name).withSourceUrl(Closure.SOURCES_DIRECTORY_NAME + "/" + name).appendTo(bundleOut, info, code);
214-
bundleOut.append("\n");
222+
writer.append("//").append(name).append("\n");
223+
224+
if (sourcemapContents != null) {
225+
sourceMapGenerator.setStartingPosition(writer.getLine(), 0);
226+
SourceMapConsumerV3 section = new SourceMapConsumerV3();
227+
section.parse(sourcemapContents);
228+
section.visitMappings((sourceName, symbolName, sourceStartPosition, startPosition, endPosition) -> sourceMapGenerator.addMapping(Paths.get(name).resolveSibling(sourceName).toString(), symbolName, sourceStartPosition, startPosition, endPosition));
229+
for (String source : section.getOriginalSources()) {
230+
String content = Files.readString(sourcesPath.resolve(name).resolveSibling(source));
231+
sourceMapGenerator.addSourcesContent(Paths.get(name).resolveSibling(source).toString(), content);
232+
}
233+
}
215234

235+
// append this file and a comment where it came from
236+
bundler.withPath(name).appendTo(writer, info, code);
237+
writer.append("\n");
216238
}
217239

240+
// write a reference to our new sourcemaps
241+
// writer.append("// " + writer.getLine()).append("\n");
242+
writer.append("//# sourceMappingURL=").append(sourcemapOutFileName).append('\n');
218243
}
244+
245+
// TODO hash in the name
246+
try (OutputStream outputStream = Files.newOutputStream(outputFilePath.resolveSibling(sourcemapOutFileName));
247+
BufferedWriter smOut = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
248+
sourceMapGenerator.appendTo(smOut, fileNameKey);
249+
}
250+
219251
// append dependency info to deserialize on some incremental rebuild
220252
try (OutputStream outputStream = Files.newOutputStream(context.outputPath().resolve("depInfo.json"));
221253
BufferedWriter jsonOut = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
@@ -238,6 +270,56 @@ public Task resolve(Project project, Config config) {
238270
};
239271
}
240272

273+
274+
public static class LineCountingWriter extends FilterWriter {
275+
private int line;
276+
protected LineCountingWriter(Writer out) {
277+
super(out);
278+
}
279+
280+
public int getLine() {
281+
return line;
282+
}
283+
284+
@Override
285+
public void write(int c) throws IOException {
286+
if (c == '\n') {
287+
line++;
288+
}
289+
super.write(c);
290+
}
291+
292+
@Override
293+
public void write(char[] cbuf, int off, int len) throws IOException {
294+
for (char c : cbuf) {
295+
if (c == '\n') {
296+
line++;
297+
}
298+
}
299+
super.write(cbuf, off, len);
300+
}
301+
302+
@Override
303+
public void write(String str, int off, int len) throws IOException {
304+
str.chars().skip(off).limit(len).forEach(c -> {
305+
if (c == '\n') {
306+
line++;
307+
}
308+
});
309+
super.write(str, off, len);
310+
}
311+
312+
@Override
313+
public void write(char[] cbuf) throws IOException {
314+
for (char c : cbuf) {
315+
if (c == '\n') {
316+
line++;
317+
}
318+
}
319+
super.write(cbuf);
320+
}
321+
}
322+
241323
public interface SourceSupplier {
242324
String get() throws IOException;
243325
}
@@ -309,6 +391,17 @@ public boolean getHasExternsAnnotation() {
309391
public boolean getHasNoCompileAnnotation() {
310392
return delegate.getHasNoCompileAnnotation();
311393
}
394+
395+
public String loadSourcemap(Path outPath) throws IOException {
396+
String sourceMappingUrlMarker = "//# sourceMappingURL=";
397+
int offset = getSource().lastIndexOf(sourceMappingUrlMarker);
398+
if (offset == -1) {
399+
return null;
400+
}
401+
int urlPos = offset + sourceMappingUrlMarker.length();
402+
String sourcemapName = getSource().substring(urlPos).split("\\s")[0];
403+
return Files.readString(outPath.resolve(getName()).resolveSibling(sourcemapName));
404+
}
312405
}
313406

314407
public static class DependencyInfoFormat implements DependencyInfo {

0 commit comments

Comments
 (0)