org.wisdom.maven.mojos.CSSMinifierMojo Maven / Gradle / Ivy
Show all versions of wisdom-maven-plugin Show documentation
/*
* #%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.Splitter;
import com.google.common.collect.ImmutableList;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
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.WatchingException;
import org.wisdom.maven.node.NPM;
import org.wisdom.maven.utils.WatcherUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Aggregates and minifies CSS files using clean-css.
* It takes all CSS files form the internal and external assets directories (so
* src/main/resources/assets, and src/main/assets) and minifies them.
*
* To configure aggregation, use the {@code } element.
*/
@Mojo(name = "minify-css", threadSafe = false,
requiresDependencyResolution = ResolutionScope.COMPILE,
requiresProject = true,
defaultPhase = LifecyclePhase.PROCESS_CLASSES)
public class CSSMinifierMojo extends AbstractWisdomWatcherMojo {
public static final String CLEANCSS_NPM_NAME = "clean-css";
public static final String CLEANCSS_NPM_VERSION = "3.4.6";
/**
* The NPM object.
*/
protected NPM cleancss;
/**
* Flag to determine if we skip minification.
*/
@Parameter(defaultValue = "${skipCleanCSS}")
public boolean skipCleanCSS;
/**
* Minified file extension parameter, lets the user define their own extensions to use with
* minification. Must not contain the {@literal .css} extension.
*/
@Parameter(defaultValue = "-min")
public String cssMinifierSuffix;
/**
* Configure the stylesheets processing. This element let you configure the CSS aggregation.
*/
@Parameter
protected Stylesheets stylesheets;
/**
* The Clean CSS NPM version.
*/
@Parameter(defaultValue = CLEANCSS_NPM_VERSION)
protected String cleanCssVersion;
/**
* A parameters to pass to Clean-CSS (the list of option is available here:
* https://github.com/jakubpawlowicz/clean-css). Are set by Wisdom the output file, the root path and source map.
* If minimisation is disabled, are enabled: skip-advanced, skip-aggressive-merging, keep-line-breaks,
* skip-media-merging, skip-shorthand-compacting. The list of arguments is appended to the default list.
*/
@Parameter
protected String cleanCssArguments;
/**
* An additional set of extensions where the output file (managed by other processors) should be minified. For
* instance if you have `.myth` files, and you want the output file generated by the Myth Mojo (`.css`) to be
* minified. Extensions are given without `.`.
*/
@Parameter
protected List additionalCssExtensions;
/**
* Checks if the skipCleanCSS flag has been set if so, we stop watching css files. If not we
* continue by setting our Clean CSS NPM object and calling the minify method for all css
* files found.
*
* @throws MojoExecutionException when modification fails.
*/
@Override
public void execute() throws MojoExecutionException {
if (skipCleanCSS) {
getLog().debug("Skipping CSS minification");
removeFromWatching();
return;
}
cleancss = NPM.npm(this, CLEANCSS_NPM_NAME, cleanCssVersion);
getLog().info("Clean CSS version: " + cleanCssVersion);
// Check whether or not we have a custom configuration
if (stylesheets == null) {
getLog().info("No 'stylesheets' processing configuration, minifying all '.css' files individually");
for (File file : getResources(getExtensions())) {
try {
process(file);
} catch (WatchingException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
} else {
process(stylesheets);
}
}
protected List getExtensions() {
return ImmutableList.builder().add("css").add("less")
.addAll(additionalCssExtensions == null ? Collections.emptyList() : additionalCssExtensions)
.build();
}
protected void process(Stylesheets stylesheets) throws MojoExecutionException {
if (stylesheets.getAggregations() == null || stylesheets.getAggregations().isEmpty()) {
getLog().warn("No 'aggregation' configured in the 'stylesheets' processing configuration - skip " +
"processing");
return;
}
for (Aggregation aggregation : stylesheets.getAggregations()) {
process(aggregation);
}
}
private void process(Aggregation aggregation) throws MojoExecutionException {
File output;
if (aggregation.getOutput() == null) {
output = getDefaultOutputFile(aggregation);
} else {
output = new File(aggregation.getOutput());
output = fixPath(output);
}
if (!output.getParentFile().isDirectory()) {
getLog().debug("Create directory " + output.getParentFile().getAbsolutePath() + " : "
+ output.getParentFile().mkdirs());
}
List arguments = new ArrayList<>();
arguments.add("--output");
arguments.add(output.getAbsolutePath());
arguments.add("--root");
arguments.add(getInternalAssetOutputDirectory().getAbsolutePath());
if (!aggregation.isMinification()) {
arguments.add("--skip-advanced");
arguments.add("--skip-aggressive-merging");
arguments.add("--keep-line-breaks");
arguments.add("--skip-aggressive-merging");
arguments.add("--skip-media-merging");
arguments.add("--skip-shorthand-compacting");
}
arguments.add("--source-map");
if (cleanCssArguments != null) {
arguments.addAll(Splitter.on(" ").splitToList(cleanCssArguments));
}
final Collection fileToAggregate = getFiles(aggregation);
for (File file : fileToAggregate) {
arguments.add(file.getAbsolutePath());
}
cleancss.execute("cleancss", arguments.toArray(new String[arguments.size()]));
// Cleanup if needed
if (aggregation.isRemoveIncludedFiles()) {
for (File file : fileToAggregate) {
FileUtils.deleteQuietly(file);
}
}
}
private Collection getFiles(Aggregation aggregation) throws MojoExecutionException {
List files = new ArrayList<>();
if (aggregation.getFiles() != null && !aggregation.getFiles().isEmpty()) {
for (String file : aggregation.getFiles()) {
File theFile = new File(file);
if (theFile.exists()) {
files.add(theFile);
} else {
File f = new File(getInternalAssetOutputDirectory(), file);
if (!f.exists() && !f.getName().endsWith("css")) {
// Append the extension
f = new File(getInternalAssetOutputDirectory(), file + ".css");
}
if (!f.exists()) {
throw new MojoExecutionException("Cannot compute aggregated CSS - the '" + f.getAbsolutePath() + "'" +
" file does not exist");
}
files.add(f);
}
}
return files;
}
// Else it uses a file set, it must indicate whether or not the file is in the output directory
return aggregation.getSelectedFiles(getInternalAssetOutputDirectory());
}
private File fixPath(File output) {
if (output.isAbsolute()) {
return output;
} else {
return new File(getInternalAssetOutputDirectory(), output.getPath());
}
}
protected File getDefaultOutputFile(Aggregation aggregation) {
String classifier = cssMinifierSuffix;
if (aggregation.isMinification()) {
if (stylesheets.getMinifierSuffix() != null) {
classifier = stylesheets.getMinifierSuffix();
}
} else {
classifier = "";
}
return new File(getInternalAssetOutputDirectory(), project.getArtifactId() + classifier + ".css");
}
/**
* Checks that the given file is a valid CSS files.
*
* @param file is the file.
* @return {@literal true} if the watcher is interested in being notified on an event
* attached to the given file,
* {@literal false} otherwise.
*/
@Override
public boolean accept(File file) {
return
(WatcherUtils.isInDirectory(file, WatcherUtils.getExternalAssetsSource(basedir))
|| (WatcherUtils.isInDirectory(file, WatcherUtils.getInternalAssetsSource(basedir)))
)
&& WatcherUtils.hasExtension(file, getExtensions())
&& isNotMinified(file)
&& JavaScriptCompilerMojo.isNotInLibs(file);
}
/**
* Checks to see if the file is already minified.
*
* @param file the current file we are looking at.
* @return a boolean.
*/
public boolean isNotMinified(File file) {
return !file.getName().endsWith("min.css")
&& !file.getName().endsWith(cssMinifierSuffix + ".css");
}
/**
* Minifies the created files.
*
* @param file is the file.
* @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
* return {@literal true} to let other watchers be notified.
* @throws org.wisdom.maven.WatchingException if the watcher failed to process the given file.
*/
@Override
public boolean fileCreated(File file) throws WatchingException {
if (stylesheets != null) {
try {
process(stylesheets);
} catch (MojoExecutionException e) {
throw new WatchingException("Error while aggregating or minifying CSS resources", file, e);
}
} else {
process(file);
}
return true;
}
/**
* Minifies the updated files.
*
* @param file is the file.
* @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
* returns {@literal true} to let other watchers to be notified.
* @throws org.wisdom.maven.WatchingException if the watcher failed to process the given file.
*/
@Override
public boolean fileUpdated(File file) throws WatchingException {
return fileCreated(file);
}
/**
* Cleans the output file if any.
*
* @param file the file
* @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should
* return {@literal true} to let other watchers be notified.
* @throws org.wisdom.maven.WatchingException if the watcher failed to process the given file.
*/
@Override
public boolean fileDeleted(File file) throws WatchingException {
if (isNotMinified(file)) {
File minified = getMinifiedFile(file);
FileUtils.deleteQuietly(minified);
File map = new File(minified.getParentFile(), minified.getName() + ".map");
FileUtils.deleteQuietly(map);
}
return true;
}
/**
* Overrides the parent method to manage the case where the given file is not a `.css`. In that case it should
* strips the extension and find a `.css` file.
*
* @param input the input file
* @return the filtered file (to the mirror of the file in the output directory), {@code null} if not found
*/
public File getFilteredVersion(File input) {
File out;
if (!input.getName().endsWith(".css")) {
out = getOutputFile(input, "css");
} else {
out = getOutputFile(input);
}
if (!out.isFile()) {
return null;
}
return out;
}
/**
* Minifies the CSS file using Clean CSS.
*
* @param file that we wish to minify.
* @throws WatchingException if errors occur during minification.
*/
private void process(File file) throws WatchingException {
getLog().info("Minifying CSS files from " + file.getName() + " using Clean CSS");
File filtered = getFilteredVersion(file);
if (filtered == null) {
filtered = file;
}
File output = getMinifiedFile(file);
if (output.exists()) {
FileUtils.deleteQuietly(output);
}
if (!output.getParentFile().isDirectory()) {
getLog().debug("Creating output directory for " + output.getAbsolutePath() + " : "
+ output.getParentFile().mkdirs());
}
getLog().info("Minifying " + filtered.getAbsolutePath() + " to " + output.getAbsolutePath());
try {
List arguments = new ArrayList<>();
arguments.add("--output");
arguments.add(output.getAbsolutePath());
arguments.add("--source-map");
if (cleanCssArguments != null) {
arguments.addAll(Splitter.on(' ').splitToList(cleanCssArguments));
}
// Input file:
arguments.add(filtered.getAbsolutePath());
int exit = cleancss.execute("cleancss", arguments.toArray(new String[arguments.size()]));
getLog().debug("CSS minification execution exiting with " + exit + " status");
} catch (MojoExecutionException e) {
throw new WatchingException("Error during the minification of " + filtered.getName(), e);
}
}
/**
* Creates out minified output file replacing the current extension with the minified one.
*
* @param input the file to minify.
* @return the output file where the minified code will go.
*/
protected File getMinifiedFile(File input) {
File output = getOutputFile(input);
String ext = FilenameUtils.getExtension(output.getName());
return new File(output.getParentFile().getAbsoluteFile(),
output.getName().replace("." + ext, cssMinifierSuffix + ".css"));
}
}