• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hazendaz / yuicompressor-maven-plugin / 457

17 Dec 2025 02:20AM UTC coverage: 21.127% (+11.1%) from 10.01%
457

push

github

web-flow
Update dependency maven to v3.9.12

56 of 354 branches covered (15.82%)

Branch coverage included in aggregate %.

154 of 640 relevant lines covered (24.06%)

0.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main/java/net/alchim31/maven/yuicompressor/YuiCompressorMojo.java
1
/*
2
 * YuiCompressor Maven plugin
3
 *
4
 * Copyright 2012-2025 Hazendaz.
5
 *
6
 * Licensed under the GNU Lesser General Public License (LGPL),
7
 * version 2.1 or later (the "License").
8
 * You may not use this file except in compliance with the License.
9
 * You may read the licence in the 'lgpl.txt' file in the root folder of
10
 * project or obtain a copy at
11
 *
12
 *     https://www.gnu.org/licenses/lgpl-2.1.html
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" basis,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
package net.alchim31.maven.yuicompressor;
21

22
import com.yahoo.platform.yui.compressor.CssCompressor;
23
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
24

25
import java.io.File;
26
import java.io.IOException;
27
import java.io.InputStream;
28
import java.io.InputStreamReader;
29
import java.io.OutputStreamWriter;
30
import java.nio.charset.Charset;
31
import java.nio.file.Files;
32
import java.nio.file.Path;
33
import java.util.Collection;
34
import java.util.HashSet;
35
import java.util.Locale;
36
import java.util.Set;
37
import java.util.zip.GZIPOutputStream;
38

39
import org.apache.maven.plugin.MojoExecutionException;
40
import org.apache.maven.plugins.annotations.LifecyclePhase;
41
import org.apache.maven.plugins.annotations.Mojo;
42
import org.apache.maven.plugins.annotations.Parameter;
43
import org.codehaus.plexus.util.FileUtils;
44
import org.codehaus.plexus.util.IOUtil;
45

46
/**
47
 * Apply compression on JS and CSS (using YUI Compressor).
48
 */
49
@Mojo(name = "compress", defaultPhase = LifecyclePhase.PROCESS_RESOURCES, requiresProject = true, threadSafe = true)
50
public class YuiCompressorMojo extends MojoSupport {
×
51

52
    /**
53
     * Read the input file using "encoding".
54
     */
55
    @Parameter(defaultValue = "${project.build.sourceEncoding}", property = "file.encoding")
56
    private String encoding;
57

58
    /**
59
     * The output filename suffix.
60
     */
61
    @Parameter(defaultValue = "-min", property = "maven.yuicompressor.suffix")
62
    private String suffix;
63

64
    /**
65
     * If no "suffix" must be add to output filename (maven's configuration manage empty suffix like default).
66
     */
67
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.nosuffix")
68
    private boolean nosuffix;
69

70
    /**
71
     * Insert line breaks in output after the specified column number.
72
     */
73
    @Parameter(defaultValue = "-1", property = "maven.yuicompressor.linebreakpos")
74
    private int linebreakpos;
75

76
    /** [js only] No compression. */
77
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.nocompress")
78
    private boolean nocompress;
79

80
    /**
81
     * [js only] Minify only, do not obfuscate.
82
     */
83
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.nomunge")
84
    private boolean nomunge;
85

86
    /**
87
     * [js only] Preserve unnecessary semicolons.
88
     */
89
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.preserveAllSemiColons")
90
    private boolean preserveAllSemiColons;
91

92
    /**
93
     * [js only] disable all micro optimizations.
94
     */
95
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.disableOptimizations")
96
    private boolean disableOptimizations;
97

98
    /**
99
     * force the compression of every files, else if compressed file already exists and is younger than source file,
100
     * nothing is done.
101
     */
102
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.force")
103
    private boolean force;
104

105
    /**
106
     * a list of aggregation/concatenation to do after processing, for example to create big js files that contain
107
     * several small js files. Aggregation could be done on any type of file (js, css, ..).
108
     */
109
    @Parameter
110
    private Aggregation[] aggregations;
111

112
    /**
113
     * request to create a gzipped version of the yuicompressed/aggregation files.
114
     */
115
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.gzip")
116
    private boolean gzip;
117

118
    /** gzip level. */
119
    @Parameter(defaultValue = "9", property = "maven.yuicompressor.level")
120
    private int level;
121

122
    /**
123
     * show statistics (compression ratio).
124
     */
125
    @Parameter(defaultValue = "true", property = "maven.yuicompressor.statistics")
126
    private boolean statistics;
127

128
    /** aggregate files before minify. */
129
    @Parameter(defaultValue = "false", property = "maven.yuicompressor.preProcessAggregates")
130
    private boolean preProcessAggregates;
131

132
    /** use the input file as output when the compressed file is larger than the original. */
133
    @Parameter(defaultValue = "true", property = "maven.yuicompressor.useSmallestFile")
134
    private boolean useSmallestFile;
135

136
    /** The in size total. */
137
    private long inSizeTotal;
138

139
    /** The out size total. */
140
    private long outSizeTotal;
141

142
    /** Keep track of updated files for aggregation on incremental builds. */
143
    private Set<String> incrementalFiles;
144

145
    @Override
146
    protected String[] getDefaultIncludes() {
147
        return new String[] { "**/*.css", "**/*.js" };
×
148
    }
149

150
    @Override
151
    public void beforeProcess() throws IOException {
152
        if (nosuffix) {
×
153
            suffix = "";
×
154
        }
155

156
        if (preProcessAggregates) {
×
157
            aggregate();
×
158
        }
159
    }
×
160

161
    @Override
162
    protected void afterProcess() throws IOException {
163
        if (statistics && inSizeTotal > 0) {
×
164
            getLog().info(String.format("total input (%db) -> output (%db)[%d%%]", inSizeTotal, outSizeTotal,
×
165
                    outSizeTotal * 100 / inSizeTotal));
×
166
        }
167

168
        if (!preProcessAggregates) {
×
169
            aggregate();
×
170
        }
171
    }
×
172

173
    /**
174
     * Aggregate.
175
     *
176
     * @throws IOException
177
     *             the IO exception
178
     */
179
    private void aggregate() throws IOException {
180
        if (aggregations == null) {
×
181
            return;
×
182
        }
183

184
        Set<File> previouslyIncludedFiles = new HashSet<>();
×
185
        for (Aggregation aggregation : aggregations) {
×
186
            getLog().info("generate aggregation : " + aggregation.getOutput());
×
187
            Collection<File> aggregatedFiles = aggregation.run(previouslyIncludedFiles, buildContext, incrementalFiles);
×
188
            previouslyIncludedFiles.addAll(aggregatedFiles);
×
189

190
            File gzipped = gzipIfRequested(aggregation.getOutput());
×
191
            if (statistics) {
×
192
                if (gzipped != null) {
×
193
                    getLog().info(String.format("%s (%db) -> %s (%db)[%d%%]", aggregation.getOutput().getName(),
×
194
                            aggregation.getOutput().length(), gzipped.getName(), gzipped.length(),
×
195
                            ratioOfSize(aggregation.getOutput(), gzipped)));
×
196
                } else if (aggregation.getOutput().exists()) {
×
197
                    getLog().info(String.format("%s (%db)", aggregation.getOutput().getName(),
×
198
                            aggregation.getOutput().length()));
×
199
                } else {
200
                    getLog().warn(String.format("%s not created", aggregation.getOutput().getName()));
×
201
                }
202
            }
203
        }
204
    }
×
205

206
    @Override
207
    protected void processFile(SourceFile src) throws IOException, MojoExecutionException {
208
        File inFile = src.toFile();
×
209
        getLog().debug("on incremental build only compress if input file has Delta");
×
210
        if (buildContext.isIncremental()) {
×
211
            if (!buildContext.hasDelta(inFile)) {
×
212
                if (getLog().isInfoEnabled()) {
×
213
                    getLog().info("nothing to do, " + inFile + " has no Delta");
×
214
                }
215
                return;
×
216
            }
217
            if (incrementalFiles == null) {
×
218
                incrementalFiles = new HashSet<>();
×
219
            }
220
        }
221

222
        if (getLog().isDebugEnabled()) {
×
223
            getLog().debug("compress file :" + src.toFile() + " to " + src.toDestFile(suffix));
×
224
        }
225

226
        File outFile = src.toDestFile(suffix);
×
227
        if (!nosuffix && isMinifiedFile(inFile)) {
×
228
            return;
×
229
        }
230
        getLog().debug("only compress if input file is younger than existing output file");
×
231
        if (!force && outFile.exists() && outFile.lastModified() > inFile.lastModified()) {
×
232
            if (getLog().isInfoEnabled()) {
×
233
                getLog().info("nothing to do, " + outFile
×
234
                        + " is younger than original, use 'force' option or clean your target");
235
            }
236
            return;
×
237
        }
238
        File outFileTmp = Path.of(outFile.getCanonicalPath() + ".tmp").toFile();
×
239
        FileUtils.forceDelete(outFileTmp);
×
240

241
        if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
×
242
            throw new MojoExecutionException("Cannot create resource output directory: " + outFile.getParentFile());
×
243
        }
244
        getLog().debug("use a temporary outputfile (in case in == out)");
×
245

246
        try (InputStreamReader in = new InputStreamReader(Files.newInputStream(inFile.toPath()),
×
247
                Charset.forName(encoding));
×
248
                /* outFileTmp will be deleted create with FileOutputStream */
249
                OutputStreamWriter out = new OutputStreamWriter(Files.newOutputStream(outFileTmp.toPath()),
×
250
                        Charset.forName(encoding));) {
×
251

252
            getLog().debug("start compression");
×
253
            try {
254
                if (nocompress) {
×
255
                    getLog().info("No compression is enabled");
×
256
                    IOUtil.copy(in, out);
×
257
                } else if (".js".equalsIgnoreCase(src.getExtension())) {
×
258
                    JavaScriptCompressor compressor = new JavaScriptCompressor(in, jsErrorReporter);
×
259
                    compressor.compress(out, linebreakpos, !nomunge, jswarn, preserveAllSemiColons,
×
260
                            disableOptimizations);
261
                } else if (".css".equalsIgnoreCase(src.getExtension())) {
×
262
                    compressCss(in, out);
×
263
                }
264
            } catch (IndexOutOfBoundsException e) {
×
265
                // This catch exists to not fail the build on YUICompressor bugs.
266
                // 2.4.8 seems to have issue on windows : https://github.com/yui/yuicompressor/issues/78
267
                // 2.4.8 failed to process empty file (demo01) : https://github.com/yui/yuicompressor/issues/130
268
                getLog().warn("YUICompressor failed on file: " + inFile.getName()
×
269
                        + " due to IndexOutOfBoundsException. Skipping this file.");
270
                return;
×
271
            }
×
272
            getLog().debug("end compression");
×
273
        }
×
274

275
        boolean outputIgnored = useSmallestFile && inFile.length() < outFile.length();
×
276
        if (outputIgnored) {
×
277
            FileUtils.forceDelete(outFileTmp);
×
278
            FileUtils.copyFile(inFile, outFile);
×
279
            getLog().debug("output greater than input, using original instead");
×
280
        } else {
281
            FileUtils.forceDelete(outFile);
×
282
            FileUtils.rename(outFileTmp, outFile);
×
283
            buildContext.refresh(outFile);
×
284
        }
285

286
        if (buildContext.isIncremental()) {
×
287
            incrementalFiles.add(outFile.getCanonicalPath());
×
288
        }
289

290
        File gzipped = gzipIfRequested(outFile);
×
291
        if (statistics) {
×
292
            inSizeTotal += inFile.length();
×
293
            outSizeTotal += outFile.length();
×
294

295
            String fileStatistics;
296
            if (outputIgnored) {
×
297
                fileStatistics = String.format(
×
298
                        "%s (%db) -> %s (%db)[compressed output discarded (exceeded input size)]", inFile.getName(),
×
299
                        inFile.length(), outFile.getName(), outFile.length());
×
300
            } else {
301
                fileStatistics = String.format("%s (%db) -> %s (%db)[%d%%]", inFile.getName(), inFile.length(),
×
302
                        outFile.getName(), outFile.length(), ratioOfSize(inFile, outFile));
×
303
            }
304

305
            if (gzipped != null) {
×
306
                fileStatistics = fileStatistics + String.format(" -> %s (%db)[%d%%]", gzipped.getName(),
×
307
                        gzipped.length(), ratioOfSize(inFile, gzipped));
×
308
            }
309
            getLog().info(fileStatistics);
×
310
        }
311
    }
×
312

313
    /**
314
     * Compress css.
315
     *
316
     * @param in
317
     *            the in
318
     * @param out
319
     *            the out
320
     */
321
    private void compressCss(InputStreamReader in, OutputStreamWriter out) throws IOException {
322
        try {
323
            CssCompressor compressor = new CssCompressor(in);
×
324
            compressor.compress(out, linebreakpos);
×
325
        } catch (IllegalArgumentException e) {
×
326
            throw new IllegalArgumentException(
×
327
                    "Unexpected characters found in CSS file. Ensure that the CSS file does not contain '$', and try again",
328
                    e);
329
        }
×
330
    }
×
331

332
    /**
333
     * Gzip if requested.
334
     *
335
     * @param file
336
     *            the file
337
     *
338
     * @return the file
339
     *
340
     * @throws IOException
341
     *             the IO exception
342
     */
343
    protected File gzipIfRequested(File file) throws IOException {
344
        if (!gzip || file == null || !file.exists() || ".gz".equalsIgnoreCase(FileUtils.getExtension(file.getName()))) {
×
345
            return null;
×
346
        }
347
        File gzipped = Path.of(file.getCanonicalFile() + ".gz").toFile();
×
348
        getLog().debug(String.format("create gzip version : %s", gzipped.getName()));
×
349
        try (InputStream in = Files.newInputStream(file.toPath());
×
350
                GZIPOutputStream out = new GZIPOutputStream(buildContext.newFileOutputStream(gzipped)) {
×
351
                    {
352
                        def.setLevel(level);
×
353
                    }
354
                };) {
355
            IOUtil.copy(in, out);
×
356
        }
357
        return gzipped;
×
358
    }
359

360
    /**
361
     * Ratio of size.
362
     *
363
     * @param file100
364
     *            the file 100
365
     * @param fileX
366
     *            the file X
367
     *
368
     * @return the long
369
     */
370
    protected long ratioOfSize(File file100, File fileX) {
371
        long v100 = Math.max(file100.length(), 1);
×
372
        long vX = Math.max(fileX.length(), 1);
×
373
        return vX * 100 / v100;
×
374
    }
375

376
    /**
377
     * Checks if is minified file.
378
     *
379
     * @param inFile
380
     *            the in file
381
     *
382
     * @return true, if is minified file
383
     */
384
    private boolean isMinifiedFile(File inFile) {
385
        String filename = inFile.getName().toLowerCase(Locale.getDefault());
×
386
        return filename.endsWith(suffix + ".js") || filename.endsWith(suffix + ".css");
×
387
    }
388

389
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc