com.google.javascript.jscomp.ant.CompileTask Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler Show documentation
Show all versions of closure-compiler Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
The newest version!
/*
* Copyright 2010 The Closure Compiler Authors.
*
* 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 com.google.javascript.jscomp.ant;
import static java.lang.Math.max;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import com.google.javascript.jscomp.CheckLevel;
import com.google.javascript.jscomp.CommandLineRunner;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.DependencyOptions;
import com.google.javascript.jscomp.DiagnosticGroup;
import com.google.javascript.jscomp.DiagnosticGroups;
import com.google.javascript.jscomp.MessageFormatter;
import com.google.javascript.jscomp.ModuleIdentifier;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.SourceFile;
import com.google.javascript.jscomp.SourceMap;
import com.google.javascript.jscomp.SourceMap.Format;
import com.google.javascript.jscomp.SourceMap.PrefixLocationMapping;
import com.google.javascript.jscomp.WarningLevel;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileList;
import org.apache.tools.ant.types.Parameter;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileResource;
/**
* This class implements a simple Ant task to do almost the same as
* CommandLineRunner.
*
* Most of the public methods of this class are entry points for the
* Ant code to hook into.
*/
public final class CompileTask
extends Task {
private final CompilerOptions.LanguageMode languageIn;
private CompilerOptions.LanguageMode languageOut;
private WarningLevel warningLevel;
private boolean debugOptions;
private String encoding = "UTF-8";
private Charset outputEncoding = UTF_8;
private CompilationLevel compilationLevel;
private CompilerOptions.Environment environment;
private boolean manageDependencies;
private boolean prettyPrint;
private boolean printInputDelimiter;
private boolean preferSingleQuotes;
private boolean generateExports;
private boolean replaceProperties;
private boolean forceRecompile;
private boolean angularPass;
private String replacePropertiesPrefix;
private File outputFile;
private String outputWrapper;
private File outputWrapperFile;
private final List defineParams;
private final List entryPointParams;
private final List externFileLists;
private final List sourceFileLists;
private final List sourcePaths;
private final List warnings;
private String sourceMapFormat;
private File sourceMapOutputFile;
private String sourceMapLocationMapping;
private boolean applyInputSourceMaps;
private boolean strictModeInput;
public CompileTask() {
this.languageIn = CompilerOptions.LanguageMode.STABLE_IN;
this.languageOut = CompilerOptions.LanguageMode.STABLE_OUT;
this.warningLevel = WarningLevel.DEFAULT;
this.debugOptions = false;
this.compilationLevel = CompilationLevel.SIMPLE_OPTIMIZATIONS;
this.environment = CompilerOptions.Environment.BROWSER;
this.strictModeInput = true;
this.manageDependencies = false;
this.prettyPrint = false;
this.printInputDelimiter = false;
this.preferSingleQuotes = false;
this.generateExports = false;
this.replaceProperties = false;
this.forceRecompile = false;
this.angularPass = false;
this.replacePropertiesPrefix = "closure.define.";
this.defineParams = new ArrayList<>();
this.entryPointParams = new ArrayList<>();
this.externFileLists = new ArrayList<>();
this.sourceFileLists = new ArrayList<>();
this.sourcePaths = new ArrayList<>();
this.warnings = new ArrayList<>();
}
private static CompilerOptions.LanguageMode parseLanguageMode(String value) {
CompilerOptions.LanguageMode language = CompilerOptions.LanguageMode.fromString(value);
if (language == null) {
throw new BuildException(
"Unrecognized 'languageIn' option value (" + value + ")");
}
return language;
}
/**
* Set the language to which output sources conform.
* @param value The name of the language.
* (ECMASCRIPT3, ECMASCRIPT5, ECMASCRIPT5_STRICT).
*/
public void setLanguageOut(String value) {
this.languageOut = parseLanguageMode(value);
}
/**
* Set the warning level.
* @param value The warning level by string name. (default, quiet, verbose).
*/
public void setWarning(String value) {
if (Ascii.equalsIgnoreCase("default", value)) {
this.warningLevel = WarningLevel.DEFAULT;
} else if (Ascii.equalsIgnoreCase("quiet", value)) {
this.warningLevel = WarningLevel.QUIET;
} else if (Ascii.equalsIgnoreCase("verbose", value)) {
this.warningLevel = WarningLevel.VERBOSE;
} else {
throw new BuildException(
"Unrecognized 'warning' option value (" + value + ")");
}
}
/**
* Set the environment which determines the builtin extern set.
* @param value The name of the environment.
* (BROWSER, CUSTOM).
*/
public void setEnvironment(String value) {
switch (value) {
case "BROWSER":
this.environment = CompilerOptions.Environment.BROWSER;
break;
case "CUSTOM":
this.environment = CompilerOptions.Environment.CUSTOM;
break;
default:
throw new BuildException(
"Unrecognized 'environment' option value (" + value + ")");
}
}
/**
* Enable debugging options.
* @param value True if debug mode is enabled.
*/
public void setDebug(boolean value) {
this.debugOptions = value;
}
/**
* Set the compilation level.
* @param value The optimization level by string name.
* (whitespace, simple, advanced).
*/
public void setCompilationLevel(String value) {
if (Ascii.equalsIgnoreCase("simple", value)) {
this.compilationLevel = CompilationLevel.SIMPLE_OPTIMIZATIONS;
} else if (Ascii.equalsIgnoreCase("advanced", value)) {
this.compilationLevel = CompilationLevel.ADVANCED_OPTIMIZATIONS;
} else if (Ascii.equalsIgnoreCase("whitespace", value)) {
this.compilationLevel = CompilationLevel.WHITESPACE_ONLY;
} else {
throw new BuildException(
"Unrecognized 'compilation' option value (" + value + ")");
}
}
public void setStrictModeInput(boolean strictModeInput) {
this.strictModeInput = strictModeInput;
}
public void setManageDependencies(boolean value) {
this.manageDependencies = value;
}
/**
* Set output file.
*/
public void setOutput(File value) {
this.outputFile = value;
}
/**
* Set output wrapper.
*/
public void setOutputWrapper(String value) {
this.outputWrapper = value;
}
/**
* Set output wrapper file.
*/
public void setOutputWrapperFile(File value) {
this.outputWrapperFile = value;
}
/**
* Set the replacement property prefix.
*/
public void setReplacePropertiesPrefix(String value) {
this.replacePropertiesPrefix = value;
}
/**
* Whether to replace {@code @define} lines with properties
*/
public void setReplaceProperties(boolean value) {
this.replaceProperties = value;
}
/**
* Set input file encoding
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Set output file encoding
*/
public void setOutputEncoding(String outputEncoding) {
this.outputEncoding = Charset.forName(outputEncoding);
}
/**
* Set pretty print formatting option
*/
public void setPrettyPrint(boolean pretty) {
this.prettyPrint = pretty;
}
/**
* Set print input delimiter formatting option
*/
public void setPrintInputDelimiter(boolean print) {
this.printInputDelimiter = print;
}
/**
* Normally, when there are an equal number of single and double quotes
* in a string, the compiler will use double quotes. Set this to true
* to prefer single quotes.
*/
public void setPreferSingleQuotes(boolean singlequotes) {
this.preferSingleQuotes = singlequotes;
}
/**
* Set force recompile option
*/
public void setForceRecompile(boolean forceRecompile) {
this.forceRecompile = forceRecompile;
}
public void setAngularPass(boolean angularPass) {
this.angularPass = angularPass;
}
/**
* Set generateExports option
*/
public void setGenerateExports(boolean generateExports) {
this.generateExports = generateExports;
}
/**
* Sets the externs file.
*/
public void addExterns(FileList list) {
this.externFileLists.add(list);
}
/**
* Adds a {@code } entry
*
* Each warning entry must have two attributes, group and level. Group must
* contain one of the constants from DiagnosticGroups (e.g.,
* "ACCESS_CONTROLS"), while level must contain one of the CheckLevel
* constants ("ERROR", "WARNING" or "OFF").
*/
public void addWarning(Warning warning) {
this.warnings.add(warning);
}
/**
* Adds a {@code } entry
*
* Each entrypoint entry must have one attribute, name.
*/
public void addEntryPoint(Parameter entrypoint) {
this.entryPointParams.add(entrypoint);
}
/**
* Sets the source files.
*/
public void addSources(FileList list) {
this.sourceFileLists.add(list);
}
/**
* Adds a {@code } entry.
*/
public void addPath(Path list) {
this.sourcePaths.add(list);
}
@Override
public void execute() {
if (this.outputFile == null) {
throw new BuildException("outputFile attribute must be set");
}
Compiler.setLoggingLevel(Level.OFF);
CompilerOptions options = createCompilerOptions();
Compiler compiler = createCompiler(options);
List externs = findExternFiles(options);
List sources = findSourceFiles();
if (isStale() || forceRecompile) {
log("Compiling " + sources.size() + " file(s) with " +
externs.size() + " extern(s)");
Result result = compiler.compile(externs, sources, options);
if (result.success) {
StringBuilder source = new StringBuilder(compiler.toSource());
if (this.outputWrapperFile != null) {
try {
this.outputWrapper = Files.asCharSource(this.outputWrapperFile, UTF_8).read();
} catch (Exception e) {
throw new BuildException("Invalid output_wrapper_file specified.", e);
}
}
if (this.outputWrapper != null) {
int pos = this.outputWrapper.indexOf(CommandLineRunner.OUTPUT_MARKER);
if (pos > -1) {
String prefix = this.outputWrapper.substring(0, pos);
source.insert(0, prefix);
// end of outputWrapper
int suffixStart = pos + CommandLineRunner.OUTPUT_MARKER.length();
String suffix = this.outputWrapper.substring(suffixStart);
source.append(suffix);
} else {
throw new BuildException("Invalid output_wrapper specified. " +
"Missing '" + CommandLineRunner.OUTPUT_MARKER + "'.");
}
}
if (result.sourceMap != null) {
flushSourceMap(result.sourceMap);
}
writeResult(source.toString());
} else {
throw new BuildException("Compilation failed.");
}
} else {
log("None of the files changed. Compilation skipped.");
}
}
private void flushSourceMap(SourceMap sourceMap) {
try (Writer out = Files.newWriter(sourceMapOutputFile, UTF_8)) {
sourceMap.appendTo(out, outputFile.getName());
} catch (IOException e) {
throw new BuildException("Cannot write sourcemap to file.", e);
}
}
private CompilerOptions createCompilerOptions() {
CompilerOptions options = new CompilerOptions();
this.compilationLevel.setOptionsForCompilationLevel(options);
if (this.debugOptions) {
this.compilationLevel.setDebugOptionsForCompilationLevel(options);
}
options.setEnvironment(this.environment);
options.setStrictModeInput(this.strictModeInput);
options.setPrettyPrint(this.prettyPrint);
options.setPrintInputDelimiter(this.printInputDelimiter);
options.setPreferSingleQuotes(this.preferSingleQuotes);
options.setGenerateExports(this.generateExports);
options.setLanguageIn(this.languageIn);
options.setLanguageOut(this.languageOut);
options.setOutputCharset(this.outputEncoding);
this.warningLevel.setOptionsForWarningLevel(options);
options.setDependencyOptions(
manageDependencies
? DependencyOptions.pruneLegacyForEntryPoints(ImmutableList.of())
: DependencyOptions.sortOnly());
convertEntryPointParameters(options);
options.setTrustedStrings(true);
options.setAngularPass(angularPass);
if (replaceProperties) {
convertPropertiesMap(options);
}
convertDefineParameters(options);
for (Warning warning : warnings) {
CheckLevel level = warning.getLevel();
String groupName = warning.getGroup();
DiagnosticGroup group = DiagnosticGroups.forName(groupName);
if (group == null) {
throw new BuildException(
"Unrecognized 'warning' option value (" + groupName + ")");
}
options.setWarningLevel(group, level);
}
if (!Strings.isNullOrEmpty(sourceMapFormat)) {
options.setSourceMapFormat(Format.valueOf(sourceMapFormat));
}
if (!Strings.isNullOrEmpty(sourceMapLocationMapping)) {
String[] tokens = sourceMapLocationMapping.split("\\|", -1);
PrefixLocationMapping lm = new PrefixLocationMapping(tokens[0], tokens[1]);
options.setSourceMapLocationMappings(Arrays.asList(lm));
}
options.setApplyInputSourceMaps(applyInputSourceMaps);
if (sourceMapOutputFile != null) {
File parentFile = sourceMapOutputFile.getParentFile();
if (parentFile.mkdirs()) {
log("Created missing parent directory " + parentFile, Project.MSG_DEBUG);
}
options.setSourceMapOutputPath(parentFile.getAbsolutePath());
}
return options;
}
/**
* Creates a new {@code } nested element. Supports name and value
* attributes.
*/
public Parameter createDefine() {
Parameter param = new Parameter();
defineParams.add(param);
return param;
}
/**
* Creates a new {@code } nested element. Supports name
* attribute.
*/
public Parameter createEntryPoint() {
Parameter param = new Parameter();
entryPointParams.add(param);
return param;
}
/**
* Converts {@code } nested elements into Compiler {@code @define}
* replacements. Note: unlike project properties, {@code } elements
* do not need to be named starting with the replacement prefix.
*/
private void convertDefineParameters(CompilerOptions options) {
for (Parameter p : defineParams) {
String key = p.getName();
Object value = p.getValue();
if (!setDefine(options, key, value)) {
log("Unexpected @define value for name=" + key + "; value=" + value);
}
}
}
/**
* Converts {@code } nested elements into Compiler entrypoint
* replacements.
*/
private void convertEntryPointParameters(CompilerOptions options) {
ImmutableList.Builder entryPointsBuilder = ImmutableList.builder();
for (Parameter p : entryPointParams) {
entryPointsBuilder.add(ModuleIdentifier.forClosure(p.getName()));
}
if (this.manageDependencies) {
options.setDependencyOptions(
DependencyOptions.pruneLegacyForEntryPoints(entryPointsBuilder.build()));
}
}
/**
* Converts project properties beginning with the replacement prefix into Compiler {@code @define}
* replacements.
*/
private void convertPropertiesMap(CompilerOptions options) {
@SuppressWarnings("unchecked")
Map props = getProject().getProperties();
for (Map.Entry entry : props.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key.startsWith(replacePropertiesPrefix)) {
key = key.substring(replacePropertiesPrefix.length());
if (!setDefine(options, key, value)) {
log("Unexpected property value for key=" + key + "; value=" + value);
}
}
}
}
/**
* Maps Ant-style values (e.g., from Properties) into expected
* Closure {@code @define} literals
*
* @return True if the {@code @define} replacement succeeded, false if
* the variable's value could not be mapped properly.
*/
private boolean setDefine(CompilerOptions options,
String key, Object value) {
boolean success = false;
if (value instanceof String) {
final boolean isTrue = "true".equals(value);
final boolean isFalse = "false".equals(value);
if (isTrue || isFalse) {
options.setDefineToBooleanLiteral(key, isTrue);
} else {
try {
double dblTemp = Double.parseDouble((String) value);
options.setDefineToDoubleLiteral(key, dblTemp);
} catch (NumberFormatException nfe) {
// Not a number, assume string
options.setDefineToStringLiteral(key, (String) value);
}
}
success = true;
} else if (value instanceof Boolean) {
options.setDefineToBooleanLiteral(key, (Boolean) value);
success = true;
} else if (value instanceof Integer) {
options.setDefineToNumberLiteral(key, (Integer) value);
success = true;
} else if (value instanceof Double) {
options.setDefineToDoubleLiteral(key, (Double) value);
success = true;
}
return success;
}
private Compiler createCompiler(CompilerOptions options) {
Compiler compiler = new Compiler();
MessageFormatter formatter =
options.getErrorFormat().toFormatter(compiler, false);
AntErrorManager errorManager = new AntErrorManager(formatter, this);
compiler.setErrorManager(errorManager);
return compiler;
}
private List findExternFiles(CompilerOptions options) {
List files = new ArrayList<>();
files.addAll(getBuiltinExterns(options));
for (FileList list : this.externFileLists) {
files.addAll(findJavaScriptFiles(list));
}
return files;
}
private List findSourceFiles() {
List files = new ArrayList<>();
for (FileList list : this.sourceFileLists) {
files.addAll(findJavaScriptFiles(list));
}
for (Path list : this.sourcePaths) {
files.addAll(findJavaScriptFiles(list));
}
return files;
}
/**
* Translates an Ant resource collection into the file list format that
* the compiler expects.
*/
private List findJavaScriptFiles(ResourceCollection rc) {
List files = new ArrayList<>();
Iterator iter = rc.iterator();
while (iter.hasNext()) {
FileResource fr = (FileResource) iter.next();
// Construct path to file, relative to current working directory.
java.nio.file.Path path =
java.nio.file.Path.of("").toAbsolutePath().relativize(fr.getFile().toPath());
files.add(SourceFile.fromPath(path, Charset.forName(encoding)));
}
return files;
}
/**
* Gets the default externs set.
*
* Adapted from {@link CommandLineRunner}.
*/
private List getBuiltinExterns(CompilerOptions options) {
try {
return CommandLineRunner.getBuiltinExterns(options.getEnvironment());
} catch (IOException e) {
throw new BuildException(e);
}
}
private void writeResult(String source) {
if (this.outputFile.getParentFile().mkdirs()) {
log("Created missing parent directory " +
this.outputFile.getParentFile(), Project.MSG_DEBUG);
}
try (OutputStreamWriter out =
new OutputStreamWriter(new FileOutputStream(this.outputFile), outputEncoding)) {
out.append(source);
} catch (IOException e) {
throw new BuildException(e);
}
log("Compiled JavaScript written to " + this.outputFile.getAbsolutePath(),
Project.MSG_DEBUG);
}
/**
* Determine if compilation must actually happen, i.e. if any input file
* (extern or source) has changed after the outputFile was last modified.
*
* @return true if compilation should happen
*/
private boolean isStale() {
long lastRun = outputFile.lastModified();
long sourcesLastModified =
max(getLastModifiedTime(this.sourceFileLists), getLastModifiedTime(this.sourcePaths));
long externsLastModified = getLastModifiedTime(this.externFileLists);
return lastRun <= sourcesLastModified || lastRun <= externsLastModified;
}
/**
* Returns the most recent modified timestamp of the file collection.
*
* Note: this must be combined into one method to account for both
* Path and FileList erasure types.
*
* @param fileLists Collection of FileList or Path
* @return Most recent modified timestamp
*/
private long getLastModifiedTime(List fileLists) {
long lastModified = 0;
for (Object entry : fileLists) {
if (entry instanceof FileList) {
FileList list = (FileList) entry;
for (String fileName : list.getFiles(this.getProject())) {
File path = list.getDir(this.getProject());
File file = new File(path, fileName);
lastModified = max(getLastModifiedTime(file), lastModified);
}
} else if (entry instanceof Path) {
Path path = (Path) entry;
for (String src : path.list()) {
File file = new File(src);
lastModified = max(getLastModifiedTime(file), lastModified);
}
}
}
return lastModified;
}
/**
* Returns the last modified timestamp of the given File.
*/
private static long getLastModifiedTime(File file) {
long fileLastModified = file.lastModified();
// If the file is absent, we don't know if it changed (maybe was deleted),
// so assume it has just changed.
if (fileLastModified == 0) {
fileLastModified = new Date().getTime();
}
return fileLastModified;
}
public void setSourceMapFormat(String format) {
this.sourceMapFormat = format;
}
public void setSourceMapOutputFile(File sourceMapOutputFile) {
this.sourceMapOutputFile = sourceMapOutputFile;
}
public void setSourceMapLocationMapping(String mapping) {
this.sourceMapLocationMapping = mapping;
}
}