All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.buckelieg.minify.plugin.MinifyMojo Maven / Gradle / Ivy

The newest version!
/*
 * Minify Maven Plugin
 * https://github.com/samaxes/minify-maven-plugin
 *
 * Copyright (c) 2009 samaxes.com
 *
 * 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.github.buckelieg.minify.plugin;

import com.github.buckelieg.minify.common.Aggregation;
import com.github.buckelieg.minify.common.AggregationConfiguration;
import com.github.buckelieg.minify.common.ClosureConfig;
import com.github.buckelieg.minify.common.YuiConfig;
import com.google.common.base.Strings;
import com.google.gson.Gson;
import com.google.javascript.jscomp.*;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static com.google.common.collect.Lists.newArrayList;

/**
 * Goal for combining and minifying CSS and JavaScript files.
 */
@Mojo(name = "minify", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, threadSafe = true)
public class MinifyMojo extends AbstractMojo {

    /**
     * Engine used for minification.
     */
    public enum Engine {
        /**
         * YUI Compressor
         */
        YUI,
        /**
         * Google Closure Compiler
         */
        CLOSURE
    }

    public enum Mode {
        PARALLEL, SEQUENTIAL
    }

    /* ************** */
    /* Global Options */
    /* ************** */

    /**
     * Display additional informational messages and warnings.
     */
    @Parameter(property = "verbose", defaultValue = "false")
    private boolean verbose;

    /**
     * Size of the buffer used to read source files.
     */
    @Parameter(property = "bufferSize", defaultValue = "4096")
    private int bufferSize;

    /**
     * If a supported character set is specified, it will be used to read the input file. Otherwise, it will assume that
     * the platform's default character set is being used. The output file is encoded using the same character set.
* See the IANA Charset Registry for a list of valid * encoding types. * * @since 1.3.2 */ @Parameter(property = "charset", defaultValue = "${project.build.sourceEncoding}") private String charset; /** * The output file name suffix. * * @since 1.3.2 */ @Parameter(property = "suffix", defaultValue = ".min") private String suffix; /** * Do not append a suffix to the minified output file name, independently of the value in the {@code suffix} * parameter.
* Warning: when both the options {@code nosuffix} and {@code skipMerge} are set to {@code true}, * the plugin execution phase needs to be set to {@code package}, otherwise the output files will be overridden by * the source files during the packaging. * * @since 1.7 */ @Parameter(property = "nosuffix", defaultValue = "false") private boolean nosuffix; /** * Skip the merge step. Minification will be applied to each source file individually. * * @since 1.5.2 */ @Parameter(property = "skipMerge", defaultValue = "false") private boolean skipMerge; /** * Skip the minify step. Useful when merging files that are already minified. * * @since 1.5.2 */ @Parameter(property = "skipMinify", defaultValue = "false") private boolean skipMinify; /** * Skip errors (of CLOSURE JS/CSS validation tasks) * * @since 1.5.2 */ @Parameter(property = "closureSkipErrors", defaultValue = "false") private boolean closureSkipErrors; /** * Webapp source directory. */ @Parameter(property = "webappSourceDir", defaultValue = "${basedir}/src/main/webapp") private String webappSourceDir; /** * Webapp target directory. */ @Parameter(property = "webappTargetDir", defaultValue = "${project.build.directory}/${project.build.finalName}") private String webappTargetDir; /** * Specify aggregations in an external JSON formatted config file. * * @since 1.7.5 */ @Parameter(property = "bundleConfiguration") private String bundleConfiguration; /* *********** */ /* CSS Options */ /* *********** */ /** * CSS source directory. */ @Parameter(property = "cssSourceDir", defaultValue = "css") private String cssSourceDir; /** * CSS source file names list. */ @Parameter(property = "cssSourceFiles", alias = "cssFiles") private ArrayList cssSourceFiles; /** * CSS files to include. Specified as fileset patterns which are relative to the CSS source directory. * * @since 1.2 */ @Parameter(property = "cssSourceIncludes", alias = "cssIncludes") private ArrayList cssSourceIncludes; /** * CSS files to exclude. Specified as fileset patterns which are relative to the CSS source directory. * * @since 1.2 */ @Parameter(property = "cssSourceExcludes", alias = "cssExcludes") private ArrayList cssSourceExcludes; /** * CSS target directory. Takes the same value as {@code cssSourceDir} when empty. * * @since 1.3.2 */ @Parameter(property = "cssTargetDir") private String cssTargetDir; /** * CSS output file name. */ @Parameter(property = "cssFinalFile", defaultValue = "style.css") private String cssFinalFile; /** * Define the CSS compressor engine to use.
* Possible values are: * * * @since 1.7.1 */ @Parameter(property = "cssEngine", defaultValue = "YUI") private Engine cssEngine; /* ****************** */ /* JavaScript Options */ /* ****************** */ /** * JavaScript source directory. */ @Parameter(property = "jsSourceDir", defaultValue = "js") private String jsSourceDir; /** * JavaScript source file names list. */ @Parameter(property = "jsSourceFiles", alias = "jsFiles") private ArrayList jsSourceFiles; /** * JavaScript files to include. Specified as fileset patterns which are relative to the JavaScript source directory. * * @since 1.2 */ @Parameter(property = "jsSourceIncludes", alias = "jsIncludes") private ArrayList jsSourceIncludes; /** * JavaScript files to exclude. Specified as fileset patterns which are relative to the JavaScript source directory. * * @since 1.2 */ @Parameter(property = "jsSourceExcludes", alias = "jsExcludes") private ArrayList jsSourceExcludes; /** * JavaScript target directory. Takes the same value as {@code jsSourceDir} when empty. * * @since 1.3.2 */ @Parameter(property = "jsTargetDir") private String jsTargetDir; /** * JavaScript output file name. */ @Parameter(property = "jsFinalFile", defaultValue = "script.js") private String jsFinalFile; /** * Define the JavaScript compressor engine to use.
* Possible values are: * * * @since 1.6 */ @Parameter(property = "jsEngine", defaultValue = "YUI") private Engine jsEngine; /* *************************** */ /* YUI Compressor Only Options */ /* *************************** */ /** * Some source control tools don't like files containing lines longer than, say 8000 characters. The line-break * option is used in that case to split long lines after a specific column. It can also be used to make the code * more readable and easier to debug. Specify {@code 0} to get a line break after each semi-colon in JavaScript, and * after each rule in CSS. Specify {@code -1} to disallow line breaks. */ @Parameter(property = "yuiLineBreak", defaultValue = "-1") private int yuiLineBreak; /** * Minify only. Do not obfuscate local symbols. */ @Parameter(property = "yuiNoMunge", defaultValue = "false") private boolean yuiNoMunge; /** * Preserve unnecessary semicolons (such as right before a '}'). This option is useful when compressed code has to * be run through JSLint. */ @Parameter(property = "yuiPreserveSemicolons", defaultValue = "false") private boolean yuiPreserveSemicolons; /** * Disable all the built-in micro-optimizations. */ @Parameter(property = "yuiDisableOptimizations", defaultValue = "false") private boolean yuiDisableOptimizations; /* ************************************ */ /* Google Closure Compiler Only Options */ /* ************************************ */ /** * Refers to which version of ECMAScript to assume when checking for errors in your code.
* Possible values are: *
    *
  • {@code ECMASCRIPT3}: Checks code assuming ECMAScript 3 compliance, and gives errors for code using features only present in later versions of ECMAScript.
  • *
  • {@code ECMASCRIPT5}: Checks code assuming ECMAScript 5 compliance, allowing new features not present in ECMAScript 3, and gives errors for code using features only present in later versions of ECMAScript.
  • *
  • {@code ECMASCRIPT5_STRICT}: Like {@code ECMASCRIPT5} but assumes compliance with strict mode ({@code 'use strict';}).
  • *
  • {@code ECMASCRIPT_2015}: ECMAScript standard approved in 2015.
  • *
  • {@code ECMASCRIPT6_TYPED}: A superset of ES6 which adds Typescript-style type declarations. Always strict.
  • *
  • {@code ECMASCRIPT_2016}: ECMAScript standard approved in 2016. Adds the exponent operator (**).
  • *
  • {@code ECMASCRIPT_2017}: ECMAScript standard approved in 2017. Adds async/await and other syntax.
  • *
  • {@code ECMASCRIPT_2018}: ECMAScript standard approved in 2018. Adds "..." in object literals/patterns.
  • *
  • {@code ECMASCRIPT_2019}: ECMAScript standard approved in 2019. Adds catch blocks with no error binding.
  • *
  • {@code ECMASCRIPT_NEXT}: ECMAScript latest draft standard..
  • *
  • {@code STABLE} (default value): Use stable features.
  • *
  • {@code NO_TRANSPILE}: For languageOut only. The same language mode as the input.
  • *
  • {@code UNSUPPORTED}: For testing only. Features that can be parsed but cannot be understood by the rest of the compiler yet.
  • *
* * @since 1.7.2 */ @Parameter(property = "closureLanguageIn", defaultValue = "STABLE") private String closureLanguageIn; /** * Refers to which version of ECMAScript your code will be returned in.
* It accepts the same options as {@code closureLanguageIn} and is used to transpile between different levels of ECMAScript. * Defaults to: {@code NO_TRANSPILE} * @since 1.7.5 */ @Parameter(property = "closureLanguageOut", defaultValue = "NO_TRANSPILE") private String closureLanguageOut; /** * Determines the set of builtin externs to load.
* Options: BROWSER, CUSTOM. * * @since 1.7.5 */ @Parameter(property = "closureEnvironment", defaultValue = "BROWSER") private CompilerOptions.Environment closureEnvironment; /** * The degree of compression and optimization to apply to your JavaScript.
* There are three possible compilation levels: *
    *
  • {@code WHITESPACE_ONLY}: Just removes whitespace and comments from your JavaScript.
  • *
  • {@code SIMPLE_OPTIMIZATIONS}: Performs compression and optimization that does not interfere with the * interaction between the compiled JavaScript and other JavaScript. This level renames only local variables.
  • *
  • {@code ADVANCED_OPTIMIZATIONS}: Achieves the highest level of compression by renaming symbols in your * JavaScript. When using {@code ADVANCED_OPTIMIZATIONS} compilation you must perform extra steps to preserve * references to external symbols. See Advanced Compilation and * Externs for more information about {@code ADVANCED_OPTIMIZATIONS}.
  • *
* * @since 1.7.2 */ @Parameter(property = "closureCompilationLevel", defaultValue = "SIMPLE_OPTIMIZATIONS") private String closureCompilationLevel; /** * List of JavaScript files containing code that declares function names or other symbols. Use * {@code closureExterns} to preserve symbols that are defined outside of the code you are compiling. The * {@code closureExterns} parameter only has an effect if you are using a {@code CompilationLevel} of * {@code ADVANCED_OPTIMIZATIONS}.
* These file names are relative to {@link #webappSourceDir} directory. * * @since 1.7.2 */ @Parameter(property = "closureExterns") private ArrayList closureExterns; /** * Collects information mapping the generated (compiled) source back to its original source for debugging purposes.
* Please visit Source Map Revision 3 Proposal for more information. * * @since 1.7.3 */ @Parameter(property = "closureCreateSourceMap", defaultValue = "false") private boolean closureCreateSourceMap; /** * Enables or disables sorting mode for Closure Library dependencies.
* If {@code true}, automatically sort dependencies so that a file that {@code goog.provides} symbol X will always come * before a file that {@code goog.requires} symbol X. * * @since 1.7.4 */ @Parameter(property = "closureSortDependencies", defaultValue = "false") private boolean closureSortDependencies; /** * Treat certain warnings as the specified CheckLevel: *
    *
  • {@code ERROR}: Makes all warnings of the given group to build-breaking error.
  • *
  • {@code WARNING}: Makes all warnings of the given group a non-breaking warning.
  • *
  • {@code OFF}: Silences all warnings of the given group.
  • *
* Example: *

     * <closureWarningLevels>
     *     <nonStandardJsDocs>OFF</nonStandardJsDocs>
     * </closureWarningLevels>
     * 
* For the complete list of diagnostic groups please visit https://github.com/google/closure-compiler/wiki/Warnings. * * @since 1.7.5 */ @Parameter(property = "closureWarningLevels") private HashMap closureWarningLevels; /** * Generate {@code $inject} properties for AngularJS for functions annotated with {@code @ngInject}. * * @since 1.7.3 */ @Parameter(property = "closureAngularPass", defaultValue = "false") private boolean closureAngularPass; /** * A whitelist of tag names in JSDoc. Needed to support JSDoc extensions like ngdoc. * * @since 1.7.5 */ @Parameter(property = "closureExtraAnnotations") private ArrayList closureExtraAnnotations; /** * Override the value of variables annotated with {@code @define}.
* The format is: *

     * <define>
     *     <name>value</name>
     * </define>
     * 
* where {@code } is the name of a {@code @define} variable and {@code value} is a boolean, number or string. * * @since 1.7.5 */ @Parameter(property = "closureDefine") private HashMap closureDefine; /** * Processing mode. * Possible values are: *
    *
  • {@code PARALLEL}: files processing will be run in parallel mode
  • *
  • {@code SEQUENTIAL}: files processing will be run in sequential mode
  • *
*/ @Parameter(defaultValue = "PARALLEL") private Mode executionMode; /** * Executed when the goal is invoked, it will first invoke a parallel lifecycle, ending at the given phase. */ @Override public void execute() throws MojoExecutionException, MojoFailureException { if (skipMerge && skipMinify) { getLog().warn("Both merge and minify steps are configured to be skipped."); return; } fillOptionalValues(); YuiConfig yuiConfig = fillYuiConfig(); ClosureConfig closureConfig = fillClosureConfig(); Collection processFilesTasks; try { processFilesTasks = createTasks(yuiConfig, closureConfig); } catch (FileNotFoundException e) { throw new MojoFailureException(e.getMessage(), e); } ExecutorService executor = Mode.PARALLEL == executionMode ? Executors.newFixedThreadPool(processFilesTasks.size()) : Executors.newSingleThreadExecutor(); try { List> futures = executor.invokeAll(processFilesTasks); for (Future future : futures) { try { future.get(); } catch (ExecutionException e) { throw new MojoExecutionException(e.getMessage(), e); } } executor.shutdown(); } catch (InterruptedException e) { executor.shutdownNow(); throw new MojoExecutionException(e.getMessage(), e); } } private void fillOptionalValues() { if (Strings.isNullOrEmpty(cssTargetDir)) { cssTargetDir = cssSourceDir; } if (Strings.isNullOrEmpty(jsTargetDir)) { jsTargetDir = jsSourceDir; } if (Strings.isNullOrEmpty(charset)) { charset = Charset.defaultCharset().name(); } } private YuiConfig fillYuiConfig() { return new YuiConfig(yuiLineBreak, !yuiNoMunge, yuiPreserveSemicolons, yuiDisableOptimizations); } private ClosureConfig fillClosureConfig() throws MojoFailureException { DependencyOptions dependencyOptions = closureSortDependencies ? DependencyOptions.sortOnly() : DependencyOptions.none(); List externs = new ArrayList<>(); for (String extern : closureExterns) { externs.add(SourceFile.fromFile(webappSourceDir + File.separator + extern, Charset.forName(charset))); } Map warningLevels = new HashMap<>(); for (Map.Entry warningLevel : closureWarningLevels.entrySet()) { DiagnosticGroup diagnosticGroup = DiagnosticGroups.forName(warningLevel.getKey()); if (diagnosticGroup == null) { throw new MojoFailureException("Failed to process closureWarningLevels: " + warningLevel.getKey() + " is an invalid DiagnosticGroup"); } try { CheckLevel checkLevel = CheckLevel.valueOf(warningLevel.getValue()); warningLevels.put(diagnosticGroup, checkLevel); } catch (IllegalArgumentException e) { throw new MojoFailureException("Failed to process closureWarningLevels: " + warningLevel.getKey() + " is an invalid CheckLevel"); } } return new ClosureConfig( LanguageMode.fromString(closureLanguageIn), LanguageMode.fromString(closureLanguageOut), closureEnvironment, CompilationLevel.fromString(closureCompilationLevel), dependencyOptions, externs, closureCreateSourceMap, warningLevels, closureAngularPass, closureExtraAnnotations, closureDefine); } private Collection createTasks(YuiConfig yuiConfig, ClosureConfig closureConfig) throws MojoFailureException, FileNotFoundException { List tasks = newArrayList(); if (!Strings.isNullOrEmpty(bundleConfiguration)) { // If a bundleConfiguration is defined, attempt to use that AggregationConfiguration aggregationConfiguration; try (Reader bundleConfigurationReader = new FileReader(bundleConfiguration)) { aggregationConfiguration = new Gson().fromJson(bundleConfigurationReader, AggregationConfiguration.class); } catch (IOException e) { throw new MojoFailureException("Failed to open the bundle configuration file [" + bundleConfiguration + "].", e); } for (Aggregation aggregation : aggregationConfiguration.getBundles()) { if (Aggregation.AggregationType.css.equals(aggregation.getType())) { tasks.add(createCSSTask(yuiConfig, closureConfig, aggregation.getFiles(), Collections.emptyList(), Collections.emptyList(), aggregation.getName())); } else if (Aggregation.AggregationType.js.equals(aggregation.getType())) { tasks.add(createJSTask(yuiConfig, closureConfig, aggregation.getFiles(), Collections.emptyList(), Collections.emptyList(), aggregation.getName())); } } } else { // Otherwise, fallback to the default behavior tasks.add(createCSSTask(yuiConfig, closureConfig, cssSourceFiles, cssSourceIncludes, cssSourceExcludes, cssFinalFile)); tasks.add(createJSTask(yuiConfig, closureConfig, jsSourceFiles, jsSourceIncludes, jsSourceExcludes,jsFinalFile)); } return tasks; } private ProcessFilesTask createCSSTask(YuiConfig yuiConfig, ClosureConfig closureConfig, List cssSourceFiles, List cssSourceIncludes, List cssSourceExcludes, String cssFinalFile) throws FileNotFoundException { return new ProcessCSSFilesTask(getLog(), verbose, bufferSize, Charset.forName(charset), suffix, nosuffix, skipMerge, skipMinify, closureSkipErrors, webappSourceDir, webappTargetDir, cssSourceDir, cssSourceFiles, cssSourceIncludes, cssSourceExcludes, cssTargetDir, cssFinalFile, cssEngine, yuiConfig); } private ProcessFilesTask createJSTask(YuiConfig yuiConfig, ClosureConfig closureConfig, List jsSourceFiles, List jsSourceIncludes, List jsSourceExcludes, String jsFinalFile) throws FileNotFoundException { return new ProcessJSFilesTask(getLog(), verbose, bufferSize, Charset.forName(charset), suffix, nosuffix, skipMerge, skipMinify, closureSkipErrors, webappSourceDir, webappTargetDir, jsSourceDir, jsSourceFiles, jsSourceIncludes, jsSourceExcludes, jsTargetDir, jsFinalFile, jsEngine, yuiConfig, closureConfig); } }