Maven plugin for web resource optimization

Mikhail edited this page Sep 5, 2016 · 40 revisions

Quick introduction

Goal of this plugin

The Maven plugin takes advantage of Google Closure Compiler and YUI Compressor. Google Closure Compiler produces better compression results for JavaScript files than YUI Compressor. But it can not compress CSS files, so that YUI Compressor is used for that. This plugin can help you to speed up web applications by compressing and combining JavaScript and CSS resources. It's simple configurable and powerful at the same time. The plugin was tested for a lot of web applications and has produced very good results. Each single JavaScript file was better compressed with Google Closure Compiler than with any other tool, even in simple mode (see compilation level below). There are many similar tests in the Internet. In advanced mode the difference was dramatic. But be careful by using of advanced mode by reason of some restrictions like dead code removal. Follow recommendations for Closure Compiler please to avoid unwanted surprises.

The plugin shows statistic in the output console, e.g.

[INFO] === Statistic ===========================================
[INFO] Size of original resources = 2.709 MB
[INFO] Size of optimized resources = 1.307 MB
[INFO] Optimized resources have 48.23% of original size
[INFO] =========================================================

Another goal and feature of this Maven plugin is creation of Source Maps and handling of Data URIs.

Source Maps

If you're doing front end web development, your web resources such as JavaScript and CSS files might be minificated, transpiled or compiled from a completely different language. If you now want to debug the generated code in the browser, you can not do that because the output code is obfuscated from the code you wrote. The solution is to use source maps. A good introduction to the source map can be found in the articles Introduction to JavaScript Source Maps and Enhance Your JavaScript Debugging with Cross-Browser Source Maps. Currently, the plugin only supports the creation of source maps for JavaScript files. If your generated code is JavaScript, one way to let the development tools know where to look is to add a comment to the end of the generated code which defines the sourceMappingURL - the location of the source map. For example: //# sourceMappingURL=mylib.js.map or //# sourceMappingURL=/mypath/mylib.js.map or //# sourceMappingURL=http://sourcemaps/mylib.js.map If you now open a development tool and the source map support is enabled, the browser will stream down the source map which points to the original file and show the original file instead of generated one (minificated, compiled, transpiled, etc.). Now, you can set a breakpoint in the original file and debug it as it would be delivered with you web application. Such debugging is possible due to the fact that a source map provides a way of mapping code within a generated file back to it's original position in a source file.

Data URIs

Data URIs are an interesting concept on the Web. Read "Data URIs explained" please if you don't know what it does mean. Data URIs allow any file to be embedded inline within CSS. This technique allows separate elements such as images to be fetched in a single HTTP request rather than multiple HTTP requests, what can be more efficient. Decreasing the number of requests results in better page performance. "Minimize HTTP requests" is actually the first rule of the "Yahoo! Exceptional Performance Best Practices", and it specifically mentions data URIs. The plugin allows to embed data URIs for referenced images in style sheets. Data URIs are supported for all modern browsers: Gecko-based (Firefox, SeaMonkey, Camino, etc.), WebKit-based (Safari, Google Chrome), Opera, Konqueror, Internet Explorer 8 and higher. For Internet Explorer 8 data URIs must be smaller than 32 KB. Internet Explorer 9 and higher don't have this 32 KB limitation. How does the conversion to data URIs work? Plugin reads the content of CSS files. A special java.io.Reader implementation looks for tokens #{resource[...]} in CSS files. This is a syntax for image references in JSF 2. Token should start with #{resource[ and ends with ]}. The content inside contains image path in JSF syntax. Examples:

.ui-icon-logosmall {
    background-image: url("#{resource['images/logosmall.gif']}") !important;
}

.ui-icon-aristo {
    background-image: url("#{resource['images:themeswitcher/aristo.png']}") !important;
}

In the next step the image resource for each background image is localized. Images directories are specified according to the JSF 2 specification and suit WAR as well as JAR projects. These are ${project.basedir}/src/main/webapp/resources and ${project.basedir}/src/main/resources/META-INF/resources. Every image is tried to be found in those directories. If the image is not found in the specified directories, then it doesn't get transformed. Otherwise, the image is encoded into base64 string. The encoding is performed only if the data URI string is less than 32KB in order to support IE8 browser. Images larger than that amount are not transformed. Data URIs looks like

.ui-icon-logosmall {
    background-image: url("data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgA ... ASUVORK5CYII=") !important;
}

.ui-icon-aristo {
    background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA ... BJRU5ErkJggg==") !important;
}

Supported mime-types are: image/gif, image/jpeg and image/png, so that GIF, JPEG, JPG and PNG images can be converted to data URIs.

Plugin setup and simple configuration

Maven plugin for resource optimization is available in the Maven Central repository. You can use it by adding the following dependency to your pom.xml:

<build>
  <plugins>
    ...  
    <plugin>
      <groupId>org.primefaces.extensions</groupId>
      <artifactId>resources-optimizer-maven-plugin</artifactId>
      <version>2.1.0</version>
      <executions>
        <execution>
          <id>optimize</id>
          <goals>
            <goal>optimize</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    ...
  </plugins>
</build>                

The last released version can be determined by searching in the Maven Central repository. There is a default plugin configuration described in the next section, but you need a proper configuration to be able to use this plugin rationally. A simple configuration would be as follows:

<plugin>
  <groupId>org.primefaces.extensions</groupId>
  <artifactId>resources-optimizer-maven-plugin</artifactId>
  <version>2.1.0</version>
  <executions>
    <execution>
      <id>optimize</id>
      <goals>
        <goal>optimize</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <inputDir>${project.build.directory}/your_resource_directory</inputDir>
  </configuration>        
</plugin>

And that's all in the simplest case. In this case only an input directory is specified where resources are located. JavaScript and CSS files located below this directory and its sub directories will be compressed and overwrite the existing not compressed files at the same location. This is a default behavior. Compilation level for Closure Compiler is predefined as SIMPLE_OPTIMIZATIONS and warning level is predefined as QUIET. Please refer API Reference for more details.

The more complex use case is shown below.

<plugin>
  <groupId>org.primefaces.extensions</groupId>
  <artifactId>resources-optimizer-maven-plugin</artifactId>
  <version>2.1.0</version>
  <executions>
    <execution>
      <id>optimize</id>
      <goals>
        <goal>optimize</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <compilationLevel>ADVANCED_OPTIMIZATIONS</compilationLevel>
    <warningLevel>VERBOSE</warningLevel>
    <failOnWarning>true</failOnWarning>
    <suffix>-min</suffix>
    <useDataUri>true</useDataUri>
    <languageIn>ECMASCRIPT6</languageIn>
    <languageOut>ECMASCRIPT3</languageOut>
    <resourcesSets>
      <resourcesSet>
        <inputDir>${project.build.directory}/your_resource_directory_for_js_files</inputDir>
        <includes>
          <include>**/*.js</include>
        </includes>
        <excludes>
          <exclude>**/jquery.layout.js</exclude>  // example of file exclude
          <exclude>jquery/**</exclude>  // example of directory exclude
        </excludes>
      </resourcesSet>
      <resourcesSet>
        <inputDir>${project.build.directory}/your_resource_directory_for_css_files</inputDir>
        <includes>
          <include>**/*.css</include>
        </includes>
        <aggregations>
          <aggregation>
            <outputFile>${project.build.directory}/webapp/resources/${project.artifactId}.css</outputFile>
          </aggregation>
        </aggregations>
      </resourcesSet>
    </resourcesSets>
  </configuration>        
</plugin>

This sample shows how to overwrite default configuration and work with resources sets. You can overwrite default settings: input directory, compilation level, warning level, behavior in case of warnings, file encoding, files to be included / excluded, aggregation rules, javascript language specifications and desired suffix for minified resources. See the next section for more details. Resources sets are optional and allows more configuration control. In the example above we have two resources sets. One for JavaScript files and one for CSS files. Each resources set can overwrite (redefine) settings for input directory where files are located, compilation level, warning level, includes, excludes and aggregation rules. The first resourcesSet includes JavaScript files except jQuery files which are usally already minified and can't be compressed with ADVANCED_OPTIMIZATIONS at the moment. Therefore, they are excluded. You can exclude single files or entire directories. The second resourcesSet compresses CSS files and merges them to one file according to the tag outputFile. Merging to one big file is possible from different resourcesSet as well. Merged content is always appended to an output file if the file already exists. The configuration above also shows how to enable data URI feature. To enable this feature set useDataUri flag to true.

The next example demonstrates simple settings for source map configuration.

<plugin>
    <groupId>org.primefaces.extensions</groupId>
    <artifactId>resources-optimizer-maven-plugin</artifactId>
    <version>2.1.0</version>
    <configuration>
        <sourceMap>
            <create>true</create>
            <outputDir>${project.basedir}/src/sourcemap/${project.version}</outputDir>
            <sourceMapRoot>
                https://raw.githubusercontent.com/someproject/master/src/sourcemap/${project.version}/
            </sourceMapRoot>
        </sourceMap>
    </configuration>
    ...
</plugin>

That means, a compressed file, say timeline.js, has the following line at the end:

//# sourceMappingURL=https://raw.githubusercontent.com/someproject/master/src/sourcemap/3.2.0/timeline.js.map

The source map timeline.js.map has the content:

{
    "version":3,
    "file":"timeline.js",
    "lineCount":238,
    "mappings":"A;;;;;;;;;;;;;;;;;;;;AA4DqB,WAArB,GAAI,MAAOA,MAAX,GACIA,KADJ,CACY,EADZ,CAUsB,...
    "sources":["timeline.source.js"],
    "names":["links","google","undefined","Array","prototype","indexOf","Array.prototype.indexOf",...]
}

If the browser development tools has identified that the source map is available, it will be fetched along with referenced uncompressed source file timeline.source.js. The source file appears in the development tools, you can set breakpoint(s) and debug it. Note: the value of the create tag can be set to false for development and to true for release via a Maven property.

Dive into configuration details

Configuration tag

The plugin is highly configurable. The following table gives an overview about all configuration details inside of configuration tag. It doesn't cover the configuration of resourcesSet.

Tag Type Default value Description
inputDir File ${project.build.directory}/webapp Input directory. Command-line parameter is "inputDir".
compilationLevel String SIMPLE_OPTIMIZATIONS Compilation level. Possible values are: WHITESPACE_ONLY, SIMPLE_OPTIMIZATIONS, ADVANCED_OPTIMIZATIONS. Command-line parameter is "compilationLevel".
warningLevel String QUIET Warning level. Possible values are: QUIET, DEFAULT, VERBOSE. To understand errors und warnings see this reference. Command-line parameter is "warningLevel".
encoding String UTF-8 Encoding to read files. Note: output files are ASCII encoded to avoid issues with proxy-servers. Command-line parameter is "encoding".
failOnWarning boolean false Flag whether this plugin must stop/fail on warnings.
suffix String null Suffix for compressed / aggregated files.
useDataUri boolean false Flag if images referenced in CSS files should be converted to data URIs.
includes String[] {"**/*.css", "**/*.js"} Files to be included. Files selectors follow patterns specified in org.codehaus.plexus.util.DirectoryScanner.
excludes String[] empty array Files to be excluded. Files selectors follow patterns specified in org.codehaus.plexus.util.DirectoryScanner.
aggregations Aggregation[] null Aggregations describing configurations how the files have to be aggregated to one big file (outputFile mode, s. below) or less files (subDirMode mode, s. below).
sourceMap SourceMap null Configuration of the source map for all resource sets. See more details below.
resourcesSets List<ResourcesSet> null List of resource sets describing resources to be processed (s. below)
languageIn String ECMASCRIPT3 Sets what language spec that input javascript sources conform. Options: ECMASCRIPT3, ECMASCRIPT5, ECMASCRIPT5_STRICT, ECMASCRIPT6, ECMASCRIPT6_STRICT, ECMASCRIPT6_TYPED
languageOut String NO_TRANSPILE Sets what language spec the output javascript should conform to. Options: ECMASCRIPT3, ECMASCRIPT5, ECMASCRIPT5_STRICT, ECMASCRIPT6_TYPED

ResourcesSet tag

The next table gives an overview about all configuration details inside of resourcesSet tag. resourcesSet contains of couple of the identical configuration parameters as in configuration tag. Consider the following rules please.

  1. If the same parameter was configured in configuration directly, the value of this parameter in resourcesSet takes precedence (overwrites the corresponding value in configuration).
  2. If a parameter is missing in resourcesSet, the value of the corresponding parameter in configuration will be taken into account.
Tag Type Default value Description
inputDir File null Input directory for this resource set.
compilationLevel String null Compilation level. Possible values are: WHITESPACE_ONLY, SIMPLE_OPTIMIZATIONS, ADVANCED_OPTIMIZATIONS.
warningLevel String null Warning level. Possible values are: QUIET, DEFAULT, VERBOSE. To understand errors und warnings see this reference.
useDataUri boolean false Flag if images referenced in CSS files should be converted to data URIs.
includes String[] null Files to be included. Files selectors follow patterns specified in org.codehaus.plexus.util.DirectoryScanner.
excludes String[] null Files to be excluded. Files selectors follow patterns specified in org.codehaus.plexus.util.DirectoryScanner.
aggregations Aggregation[] null Aggregations describing configurations how the files have to be aggregated to one big file (outputFile mode, s. below) or less files (subDirMode mode, s. below).
sourceMap SourceMap null Configuration of the source map for this resource set. See more details below.

Aggregation tag

The next table gives an overview about all configuration details inside of aggregation tag. The rules for duplicated / missing configuration parameters are:

  1. If a parameter is missing in aggregation, the value of the corresponding parameter in resourcesSet will be taken into account.
  2. If a parameter is missing in resourcesSet, the value of the corresponding parameter in configuration will be taken into account.
  3. If the same parameter was configured in resourcesSet and configuration, the value of this parameter in resourcesSet takes precedence.
Tag Type Default value Description
inputDir File null Input directory for this aggregation. This allows to define different directories to aggregate compressed and not compressed files simultaneously, at the same time. This makes sense if you have "production" and "development" modes and would like to use uncompressed files in the "development" mode. Uncompressed files can be better debugged and produce better error messages. See the next section with examples how to do such configuration.
subDirMode boolean false Aggregation per sub-folder. Names of aggregated files should be the same as their folder names where they are placed. Files are aggregated in the lexicographic order. If you want to aggregate files in the pre-defined order, you shoud give all files proper names. It's a good practice to use prefixes to sort them lexicographically. E.g. 0-firstfile.js, 1-seconfile.js, 2-thirdfile.js. See the next section with examples how to do such configuration.
removeIncluded boolean true Flag whether included original files must be removed.
withoutCompress boolean false Flag whether included files must be compressed or not.
outputFile File null Output file for aggregation to one big file.
prependedFile File null File to be prepended to the aggregated file.

subDirMode is probably one of the most benefits of this plugin for modular software. Resources of each module can be optimized separately in an easy way. It makes sense e.g. for component / widget development. Developers can write their scripts / cascading style sheets well-arranged and as much as they want (for better maintenance) - all files will be combined during project build. Web pages should reference combined files from the start of course.

SourceMap tag

The sourceMap tag consists of 5 possible sub tags which are listed below. The rules for duplicated / missing configuration parameters are the same as described above:

  1. If a parameter is missing in sourceMap configured for resourcesSet, the value of the corresponding parameter in configuration will be taken into account.
  2. If the same parameter was configured in resourcesSet and configuration, the value of this parameter in resourcesSet takes precedence.
Tag Type Default value Description
create boolean false Boolean flag if the source map should be created.
sourceMapRoot String null Path to the location of source maps and original source files. The path is prepended to the file name in the source mapping URL declaration //# sourceMappingURL which is appended to all minified files of corresponding resource set. The default value null means nothing will be prepended.
outputDir String ${project.build.directory}/sourcemap/ Output directory for created source maps and original source files.
detailLevel String V3 Source maps details level as Enum name. See com.google.javascript.jscomp.SourceMap.DetailLevel.
format String ALL Source maps format as Enum name. See com.google.javascript.jscomp.SourceMap.Format.

Configuration examples

Configuration examples demonstrate several common use cases.

Configuration for JAR projects

If you provide web resources in a JAR file as e.g. JSF component libraries typically do, you can configure this plugin as shown below.

<configuration>
  <inputDir>${project.build.directory}/classes/META-INF/resources/primefaces</inputDir>
  <resourcesSets>
    <resourcesSet>
      <inputDir>${project.build.directory}/classes/META-INF/resources</inputDir>
    </resourcesSet>
    <resourcesSet>
      <includes>
        <include>jquery/ui/jquery-ui.css</include>
        <include>accordion/accordion.css</include>
        ...
        <include>treetable/treetable.css</include>
        <include>wizard/wizard.css</include>
      </includes>
      <aggregations>
        <aggregation>
          <withoutCompress>true</withoutCompress>
          <outputFile>
            ${project.build.directory}/classes/META-INF/resources/primefaces/primefaces.css
          </outputFile>
        </aggregation>
      </aggregations>
    </resourcesSet>
    <resourcesSet>
      <includes>
        <include>core/core.js</include>
        <include>accordion/accordion.js</include>
        ...
        <include>treetable/treetable.js</include>
        <include>wizard/wizard.js</include>
      </includes>
      <aggregations>
        <aggregation>
          <withoutCompress>true</withoutCompress>
          <outputFile>
            ${project.build.directory}/classes/META-INF/resources/primefaces/primefaces.js
          </outputFile>
        </aggregation>
      </aggregations>
    </resourcesSet>
  </resourcesSets>
</configuration>

The next example demonstrates how the configuration for PrimeFaces Extensions project was made in the past. This configuration uses subDirMode, withoutCompress parameters and prepares compressed and uncompressed resources. Such preparation takes place by using of maven-resources-plugin which is placed in front of resources-optimizer-maven-plugin. A special JSF ResourceHandler could stream down uncompressed resources to the browser for the ProjectStage "Development".

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <executions>
    <execution>
      <id>copy-resources</id>
      <phase>generate-resources</phase>
      <goals>
        <goal>copy-resources</goal>
      </goals>
      <configuration>
        <outputDirectory>${resources.dir.uncompressed}</outputDirectory>
    <resources>
      <resource>
        <directory>
              ${project.basedir}/src/main/resources/META-INF/resources/primefaces-extensions
            </directory>
          </resource>
    </resources>
      </configuration>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.primefaces.extensions</groupId>
  <artifactId>resources-optimizer-maven-plugin</artifactId>
  <configuration>
    <resourcesSets>
      <resourcesSet>
        <includes>
      <include>imageareaselect/**</include>
      <include>layout/**</include>
      <include>tooltip/**</include>
      <include>keyfilter/**</include>
    </includes>
    <aggregations>
      <aggregation>
        <inputDir>${resources.dir.compressed}</inputDir>
        <subDirMode>true</subDirMode>
      </aggregation>
      <aggregation>
        <withoutCompress>true</withoutCompress>
        <inputDir>${resources.dir.uncompressed}</inputDir>
        <subDirMode>true</subDirMode>
      </aggregation>
        </aggregations>
      </resourcesSet>
      <resourcesSet>
        <includes>
      <include>masterdetail/masterdetail.css</include>
    </includes>
        <aggregations>
      <aggregation>
        <inputDir>${resources.dir.compressed}</inputDir>
        <outputFile>${resources.dir.compressed}/primefaces-extensions.css</outputFile>
      </aggregation>
      <aggregation>
        <withoutCompress>true</withoutCompress>
        <inputDir>${resources.dir.uncompressed}</inputDir>
        <outputFile>${resources.dir.uncompressed}/primefaces-extensions.css</outputFile>
      </aggregation>
    </aggregations>
      </resourcesSet>
      <resourcesSet>
        <includes>
      <include>core/core.js</include>
      <include>ajaxstatus/ajaxstatus.js</include>
      <include>ckeditor/widget.js</include>
      <include>imagerotateandresize/imagerotateandresize.js</include>
    </includes>
    <aggregations>
      <aggregation>
        <inputDir>${resources.dir.compressed}</inputDir>                                
        <outputFile>${resources.dir.compressed}/primefaces-extensions.js</outputFile>
      </aggregation>
      <aggregation>
        <withoutCompress>true</withoutCompress>
        <inputDir>${resources.dir.uncompressed}</inputDir>                                
        <outputFile>${resources.dir.uncompressed}/primefaces-extensions.js</outputFile>
      </aggregation>
    </aggregations>
      </resourcesSet>
    </resourcesSets>
  </configuration>
</plugin>

<properties>
  <resources.dir.compressed>${project.build.directory}/classes/META-INF/resources/primefaces-extensions</resources.dir.compressed>
  <resources.dir.uncompressed>${project.build.directory}/classes/META-INF/resources/primefaces-extensions-uncompressed</resources.dir.uncompressed>
</properties>

Configuration for WAR projects

We prepared three sample configurations. Standard WAR packaging does not expose web resources under src/main/webapp for use in other plugins and runs as a single execution. Fortunately, we can use maven-resources-plugin to copy these resources earlier in the build lifecycle, so that they can be used by the resources-optimizer-maven-plugin. The standard package usage will use then copied (modified) web resources when creating the WAR artifact. Run maven-resources-plugin with phase generate-resources before resources-optimizer-maven-plugin (phase prepare-package). A typically configuration for web projects with generated WAR files can be done with the following steps.

<build>
  <plugins>
    ...
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-resources-plugin</artifactId>
      <executions>
        <execution>
          <id>copy-resources</id>
          <phase>generate-resources</phase>
          <goals>
            <goal>copy-resources</goal>
          </goals>
          <configuration>
            <outputDirectory>${project.build.directory}/generated-webapp/resources</outputDirectory>
            <resources>
              <resource>
                <directory>${project.basedir}/src/main/webapp/resources</directory>
                <filtering>true</filtering>
                <includes>
                  <include>**/*.css</include>
                  <include>**/*.js</include>
                </includes>
              </resource>
            </resources>
          </configuration>
        </execution>
      </executions>
    </plugin>            
    <plugin>
      <groupId>org.primefaces.extensions</groupId>
      <artifactId>resources-optimizer-maven-plugin</artifactId>
      <executions>
        <execution>
          <id>optimize</id>
          <phase>prepare-package</phase>
          <goals>
            <goal>optimize</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <resourcesSets>
          <resourcesSet>
            <inputDir>${project.build.directory}/generated-webapp</inputDir>
          </resourcesSet>
        </resourcesSets>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-war-plugin</artifactId>
      <executions>
        <execution>
          <id>war</id>
          <phase>package</phase>
          <goals>
            <goal>war</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <webappDirectory>${project.build.directory}/webapp</webappDirectory>
        <warSourceDirectory>${project.basedir}/src/main/webapp</warSourceDirectory>
        <warSourceExcludes>**/resources/**/*.css,**/resources/**/*.js</warSourceExcludes>
        <webResources>
          <resource>
            <directory>${project.build.directory}/generated-webapp</directory>
          </resource>
        </webResources>
      </configuration>
    </plugin>            
  </plugins>
</build>

In the example above we set warSourceDirectory for the maven-war-plugin to ${project.build.directory}/generated-webapp because this is a working directory for the resources-optimizer-maven-plugin. Many plugins works in the same manner when building WARs. You can see this e.g. for maven-replacer-plugin. The second example shows an advanced configuration with resource aggregation.

<build>
  <plugins>
    ...
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-resources-plugin</artifactId>
      <executions>
        <execution>
          <id>copy-resources</id>
          <phase>generate-resources</phase>
          <goals>
            <goal>copy-resources</goal>
          </goals>
          <configuration>
            <outputDirectory>${project.build.directory}/webapp-resources/resources</outputDirectory>
            <resources>
              <resource>
                <directory>${project.basedir}/src/main/webapp/resources</directory>
                <includes>
                  <include>**/*.css</include>
                  <include>**/*.js</include>
                </includes>
              </resource>
            </resources>
          </configuration>
        </execution>
      </executions>
    </plugin>            
    <plugin>
      <groupId>org.primefaces.extensions</groupId>
      <artifactId>resources-optimizer-maven-plugin</artifactId>
      <executions>
        <execution>
          <id>optimize</id>
          <phase>prepare-package</phase>
          <goals>
            <goal>optimize</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <resourcesSets>
          <resourcesSet>
            <inputDir>${project.build.directory}/webapp-resources/resources/css</inputDir>
            <includes>
              <include>**/*.css</include>
            </includes>
            <aggregations>
              <aggregation>
                <outputFile>
                  ${project.build.directory}/webapp/resources/css/${project.artifactId}.css
                </outputFile>
              <aggregation>
            </aggregations>
          </resourcesSet>
          <resourcesSet>
            <inputDir>${project.build.directory}/webapp-resources/resources/js</inputDir>
            <includes>
              <include>**/*.js</include>
            </includes>
            <aggregations>
              <aggregation>
                <outputFile>
                  ${project.build.directory}/webapp/resources/js/${project.artifactId}.js
                </outputFile>
              <aggregation>
            </aggregations>
          </resourcesSet>
        </resourcesSets>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-war-plugin</artifactId>
      <executions>
        <execution>
          <id>war</id>
          <phase>package</phase>
          <goals>
            <goal>war</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <warSourceDirectory>${project.basedir}/src/main/webapp</warSourceDirectory>
        <warSourceExcludes>**/resources/**/*.css,**/resources/**/*.js</warSourceExcludes>
      </configuration>
    </plugin>            
  </plugins>
</build>

The third example only shows a configuration part for advanced techniques. You see there how to overwrite default settings for compilationLevel, warningLevel in a single resourcesSet and how to use prependedFile tag (see also FAQ).

<configuration>
  <inputDir>${project.build.directory}/classes/META-INF/resources/primefaces-extensions</inputDir>
  <failOnWarning>true</failOnWarning>
  <suffix>-min</suffix>
  <excludes>
    <exclude>**/jquery.layout.js</exclude>
    <exclude>jquery/**</exclude>
  </excludes>
  <aggregations>
    <aggregation>
      <subDirMode>true</subDirMode>
      <removeIncluded>false</removeIncluded>
    <aggregation>
  </aggregations>
  <resourcesSets>
    <resourcesSet>
      <inputDir>${project.build.directory}/webapp/resources/themes/app</inputDir>
      <includes>
        <include>**/*.css</include>
      </includes>
      <aggregations>
        <aggregation>
          <withoutCompress>true</withoutCompress>
          <outputFile>
            ${project.build.directory}/webapp/resources/themes/app/${project.artifactId}.css
          </outputFile>
        <aggregation>
      </aggregations>                
    </resourcesSet>
    <resourcesSet>
      <inputDir>${project.build.directory}/webapp/resources/js/app</inputDir>
      <compilationLevel>ADVANCED_OPTIMIZATIONS</compilationLevel>
      <warningLevel>DEFAULT</warningLevel>
      <includes>
        <include>**/*.js</include>
      </includes>
      <aggregations>
        <aggregation>
          <prependedFile>
            ${project.build.directory}/webapp/resources/js/app/license.txt
          </prependedFile>
          <outputFile>
            ${project.build.directory}/webapp/resources/js/app/${project.artifactId}.js
          </outputFile>
        <aggregation>
      </aggregations>
    </resourcesSet>            
  </resourcesSets>
</configuration>        

Frequently Asked Questions

Why I see line breaks in compressed files?

Firewalls and proxies sometimes corrupt or ignore large CSS and JavaScript files with very long lines. This Maven plugin adds line breaks every 500 characters in order to prevent this problem. The impact on code size is small. The code size penalty is even smaller when files are gzipped.

How to preserve comments in compressed files?

All comments are removed from the files by default but often it's necessary to preserve copyright and license terms of use etc. There is a simple solution to retain comments in files. To preserve comments in CSS file, simply add an ! sign after the opening \* and the comment block will remain in the compressed output.

/*! some copyright information here */

To preserve comments in JavaScript file, simply add an @license or @preserve annotation and the comment block will remain in the compressed output.

/**
 * @preserve Copyright 2015 SomeThirdParty.
 * Here is the full license text and copyright notice for this file.
 */

How to add a copyright or license comment to an combined file?

If you use an aggregation tag, you can prepend any other file containing some comments to the output file with the tag prependedFile. By means of this tag you are able to insert any comments at the beginning of the combined output file.

<resourcesSet>
  ...
  <aggregation>
    <prependedFile>${project.build.directory}/webapp/resources/js/app/license.txt</prependedFile>
    <outputFile>${project.build.directory}/webapp/resources/js/application.js</outputFile>
  </aggregation>
  ...
</resourcesSet>

Why does my JavaScript code stop working when I use ADVANCED_OPTIMIZATIONS?

Using Advanced mode usually requires some preparation and code changes. Advanced Compilation and Externs explains how to make sure your code works with ADVANCED_OPTIMIZATIONS. You will probably see warnings or errors as well if you use warning levels like DEFAULT or VERBOSE. The Google Closure Compiler validates JavaScript code. E.g. if you declare a global variable without the keyword var, you will see a warning JSC_UNDEFINED_VARIABLE. Warning level can be set in the plugin configuration by the tag warningLevel.

Are there cases where the optimization fails or produces a wrong result?

There are some rare cases causing problems. Google Closure Compiler has a well-known issue with eval() method. eval() is EVIL :-), we know, but many "old" scripts still uses this construct. This code e.g.

var node = getNode();
eval('myHandler(node)');

will not work after optimization because the variable node gets optimized (is shorten) and the string myHandler(node) stays unchanged. Constructs like

eval('a.' + b)

are problematic as well. Sure, writing a[b] would solve the problem, but we can not do that for third-party libraries. Better would be to use the same approach applied by UglifyJS. A good approach would be: "If eval() or with{} are used in some scope, then all variables in that scope and any variables in the parent scopes will remain unmangled, and any references to such variables remain unmangled as well." YUI Compressor has troubles with some bizarre constructs as well. We have figured out e.g. that using of comments and a well-known Internet Explorer hack with * sign together stops the plugin's running. This CSS snippet leads to the problem and breaks the optimization:

/* float clearing for IE6 */
* html .ui-module {
  height: 1%;
}

/* float clearing for IE7 */
* + html .ui-module {
  min-height: 1%;
}

A solution is simple - remove comments /* .... */.