/*
* #%L
* Wisdom-Framework
* %%
* Copyright (C) 2013 - 2014 Wisdom Framework
* %%
* 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.
* #L%
*/
package org.wisdom.maven.mojos;
import com.google.common.base.Strings;
import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.wisdom.maven.Constants;
import org.wisdom.maven.WatchingException;
import org.wisdom.maven.node.NPM;
import org.wisdom.maven.utils.WatcherUtils;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.wisdom.maven.node.NPM.npm;
/**
* Compiles TypeScript files to JavaScript.
* All '.ts' files from 'src/main/resources/assets' are compiled to 'target/classes/',
* while 'src/main/assets/' are compiled to 'target/wisdom/assets'.
*/
@Mojo(name = "compile-typescript", threadSafe = false,
requiresDependencyResolution = ResolutionScope.COMPILE,
requiresProject = true,
defaultPhase = LifecyclePhase.COMPILE)
public class TypeScriptCompilerMojo extends AbstractWisdomWatcherMojo implements Constants {
/**
* The typescript NPM name.
*/
public static final String TYPE_SCRIPT_NPM_NAME = "typescript";
/**
* The command to be launched.
*/
public static final String TYPE_SCRIPT_COMMAND = "tsc";
/**
* The title used for TypeScript compilation error.
*/
public static final String ERROR_TITLE = "TypeScript Compilation Error";
/**
* The regex used to extract information from typescript error message.
*/
private static final Pattern TYPESCRIPT_COMPILATION_ERROR = Pattern.compile("(.*)\\(([0-9]*),([0-9]*)\\):(.*)");
public static final String EXTENSION = "ts";
private NPM npm;
/**
* Configuration of the typescript processing.
*/
@Parameter
TypeScript typescript;
private File internalSources;
private File destinationForInternals;
private File externalSources;
private File destinationForExternals;
/**
* Finds and compiles coffeescript files.
*
* @throws MojoExecutionException if the compilation failed.
*/
@Override
public void execute() throws MojoExecutionException {
if (typescript == null) {
typescript = new TypeScript();
}
if (!typescript.isEnabled()) {
removeFromWatching();
getLog().info("Typescript processing disabled");
return;
}
npm = npm(this, TYPE_SCRIPT_NPM_NAME, typescript.getVersion());
this.internalSources = new File(basedir, MAIN_RESOURCES_DIR);
this.destinationForInternals = new File(buildDirectory, "classes");
this.externalSources = new File(basedir, ASSETS_SRC_DIR);
this.destinationForExternals = new File(getWisdomRootDirectory(), ASSETS_DIR);
try {
if (internalSources.isDirectory()) {
getLog().info("Compiling TypeScript files with 'tsc' from " + internalSources.getAbsolutePath());
processDirectory(internalSources, destinationForInternals);
}
if (externalSources.isDirectory()) {
getLog().info("Compiling TypeScript files with 'tsc' from " + externalSources.getAbsolutePath());
processDirectory(externalSources, destinationForExternals);
}
} catch (WatchingException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
/**
* Accepts all `coffee` files that are in 'src/main/resources/assets' or in 'src/main/assets/'.
*
* @param file the file
* @return {@literal true} if the file is accepted.
*/
@Override
public boolean accept(File file) {
return
(WatcherUtils.isInDirectory(file, WatcherUtils.getInternalAssetsSource(basedir))
|| (WatcherUtils.isInDirectory(file, WatcherUtils.getExternalAssetsSource(basedir)))
)
&& WatcherUtils.hasExtension(file, EXTENSION);
}
/**
* A file is created - process it.
*
* @param file the file
* @return {@literal true} as the pipeline should continue
* @throws WatchingException if the processing failed
*/
@Override
public boolean fileCreated(File file) throws WatchingException {
if (WatcherUtils.isInDirectory(file, internalSources)) {
processDirectory(internalSources, destinationForInternals);
} else if (WatcherUtils.isInDirectory(file, externalSources)) {
processDirectory(externalSources, destinationForExternals);
}
return true;
}
/**
* Process all typescripts file from the given directory. Output files are generated in the given destination.
*
* @param input the input directory
* @param destination the output directory
* @throws WatchingException if the compilation failed
*/
protected void processDirectory(File input, File destination) throws WatchingException {
if (! input.isDirectory()) {
return;
}
if (! destination.isDirectory()) {
destination.mkdirs();
}
// Now execute the compiler
// We compute the set of argument according to the Mojo's configuration.
try {
Collection<File> files = FileUtils.listFiles(input, new String[]{"ts"}, true);
if (files.isEmpty()) {
return;
}
List<String> arguments = typescript.createTypeScriptCompilerArgumentList(input, destination, files);
getLog().info("Invoking the TypeScript compiler with " + arguments);
npm.registerOutputStream(true);
int exit = npm.execute(TYPE_SCRIPT_COMMAND,
arguments.toArray(new String[arguments.size()]));
getLog().debug("TypeScript Compiler execution exiting with status: " + exit);
} catch (MojoExecutionException e) {
// If the NPM execution has caught an error stream, try to create the associated watching exception.
if (!Strings.isNullOrEmpty(npm.getLastOutputStream())) {
throw build(npm.getLastOutputStream());
} else {
throw new WatchingException(ERROR_TITLE, "Error while compiling " + input
.getAbsolutePath(), input, e);
}
}
}
/**
* Creates the Watching Exception by parsing the NPM error log.
*
* @param message the log
* @return the watching exception
*/
private WatchingException build(String message) {
String[] lines = message.split("\n");
for (String l : lines) {
if (!Strings.isNullOrEmpty(l)) {
message = l.trim();
break;
}
}
final Matcher matcher = TYPESCRIPT_COMPILATION_ERROR.matcher(message);
if (matcher.matches()) {
String path = matcher.group(1);
String line = matcher.group(2);
String character = matcher.group(3);
String reason = matcher.group(4);
File file = new File(path);
return new WatchingException(ERROR_TITLE, reason, file,
Integer.parseInt(line), Integer.parseInt(character), null);
} else {
return new WatchingException(ERROR_TITLE, message, null, null);
}
}
/**
* A file is updated - process it.
*
* @param file the file
* @return {@literal true} as the pipeline should continue
* @throws WatchingException if the processing failed
*/
@Override
public boolean fileUpdated(File file) throws WatchingException {
return fileCreated(file);
}
/**
* A file is deleted - delete the output.
* This method deletes the generated js, js.map (source map) and declaration (d.ts) files.
*
* @param file the file
* @return {@literal true} as the pipeline should continue
*/
@Override
public boolean fileDeleted(File file) {
FileUtils.deleteQuietly(getOutputFile(file, "js"));
FileUtils.deleteQuietly(getOutputFile(file, "js.map"));
FileUtils.deleteQuietly(getOutputFile(file, "d.ts"));
return true;
}
/**
* Gets the output file for the given input and the given extension.
*
* @param input the input file
* @param ext the extension
* @return the output file, may not exist
*/
public File getOutputFile(File input, String ext) {
File source;
File destination;
if (input.getAbsolutePath().startsWith(internalSources.getAbsolutePath())) {
source = internalSources;
destination = destinationForInternals;
} else if (input.getAbsolutePath().startsWith(externalSources.getAbsolutePath())) {
source = externalSources;
destination = destinationForExternals;
} else {
return null;
}
String jsFileName = input.getName().substring(0, input.getName().length() - ".ts".length()) + "." + ext;
String path = input.getParentFile().getAbsolutePath().substring(source.getAbsolutePath().length());
return new File(destination, path + "/" + jsFileName);
}
}