001/*
002 * Minify Maven Plugin
003 * https://github.com/samaxes/minify-maven-plugin
004 *
005 * Copyright (c) 2009 samaxes.com
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019package com.samaxes.maven.minify.plugin;
020
021import com.google.common.collect.Lists;
022import com.google.javascript.jscomp.*;
023import com.google.javascript.jscomp.Compiler;
024import com.samaxes.maven.minify.common.ClosureConfig;
025import com.samaxes.maven.minify.common.JavaScriptErrorReporter;
026import com.samaxes.maven.minify.common.YuiConfig;
027import com.samaxes.maven.minify.plugin.MinifyMojo.Engine;
028import com.yahoo.platform.yui.compressor.JavaScriptCompressor;
029import org.apache.maven.plugin.logging.Log;
030
031import java.io.*;
032import java.nio.charset.Charset;
033import java.util.ArrayList;
034import java.util.List;
035import java.util.Map;
036
037/**
038 * Task for merging and compressing JavaScript files.
039 */
040public class ProcessJSFilesTask extends ProcessFilesTask {
041
042    private final ClosureConfig closureConfig;
043
044    /**
045     * Task constructor.
046     *
047     * @param log             Maven plugin log
048     * @param verbose         display additional info
049     * @param bufferSize      size of the buffer used to read source files
050     * @param charset         if a character set is specified, a byte-to-char variant allows the encoding to be selected.
051     *                        Otherwise, only byte-to-byte operations are used
052     * @param suffix          final file name suffix
053     * @param nosuffix        whether to use a suffix for the minified file name or not
054     * @param skipMerge       whether to skip the merge step or not
055     * @param skipMinify      whether to skip the minify step or not
056     * @param webappSourceDir web resources source directory
057     * @param webappTargetDir web resources target directory
058     * @param inputDir        directory containing source files
059     * @param sourceFiles     list of source files to include
060     * @param sourceIncludes  list of source files to include
061     * @param sourceExcludes  list of source files to exclude
062     * @param outputDir       directory to write the final file
063     * @param outputFilename  the output file name
064     * @param engine          minify processor engine selected
065     * @param yuiConfig       YUI Compressor configuration
066     * @param closureConfig   Google Closure Compiler configuration
067     * @throws FileNotFoundException when the given source file does not exist
068     */
069    public ProcessJSFilesTask(Log log, boolean verbose, Integer bufferSize, Charset charset, String suffix,
070                              boolean nosuffix, boolean skipMerge, boolean skipMinify, String webappSourceDir,
071                              String webappTargetDir, String inputDir, List<String> sourceFiles,
072                              List<String> sourceIncludes, List<String> sourceExcludes, String outputDir,
073                              String outputFilename, Engine engine, YuiConfig yuiConfig, ClosureConfig closureConfig)
074            throws FileNotFoundException {
075        super(log, verbose, bufferSize, charset, suffix, nosuffix, skipMerge, skipMinify, webappSourceDir,
076                webappTargetDir, inputDir, sourceFiles, sourceIncludes, sourceExcludes, outputDir, outputFilename,
077                engine, yuiConfig);
078
079        this.closureConfig = closureConfig;
080    }
081
082    /**
083     * Minifies a JavaScript file. Create missing parent directories if needed.
084     *
085     * @param mergedFile   input file resulting from the merged step
086     * @param minifiedFile output file resulting from the minify step
087     * @throws IOException when the minify step fails
088     */
089    @Override
090    protected void minify(File mergedFile, File minifiedFile) throws IOException {
091        if (!minifiedFile.getParentFile().exists() && !minifiedFile.getParentFile().mkdirs()) {
092            throw new RuntimeException("Unable to create target directory for: " + minifiedFile.getParentFile());
093        }
094
095        try (InputStream in = new FileInputStream(mergedFile);
096             OutputStream out = new FileOutputStream(minifiedFile);
097             InputStreamReader reader = new InputStreamReader(in, charset);
098             OutputStreamWriter writer = new OutputStreamWriter(out, charset)) {
099            log.info("Creating the minified file [" + (verbose ? minifiedFile.getPath() : minifiedFile.getName()) + "].");
100
101            switch (engine) {
102                case CLOSURE:
103                    log.debug("Using Google Closure Compiler engine.");
104
105                    CompilerOptions options = new CompilerOptions();
106                    closureConfig.getCompilationLevel().setOptionsForCompilationLevel(options);
107                    options.setOutputCharset(charset);
108                    options.setLanguageIn(closureConfig.getLanguageIn());
109                    options.setLanguageOut(closureConfig.getLanguageOut());
110                    options.setDependencyOptions(closureConfig.getDependencyOptions());
111                    options.setColorizeErrorOutput(closureConfig.getColorizeErrorOutput());
112                    options.setAngularPass(closureConfig.getAngularPass());
113                    options.setExtraAnnotationNames(closureConfig.getExtraAnnotations());
114                    options.setDefineReplacements(closureConfig.getDefineReplacements());
115                    // options.setRewritePolyfills(closureConfig.getLanguageIn().isEs6OrHigher());
116
117                    File sourceMapResult = new File(minifiedFile.getPath() + ".map");
118                    if (closureConfig.getSourceMapFormat() != null) {
119                        options.setSourceMapFormat(closureConfig.getSourceMapFormat());
120                        options.setSourceMapOutputPath(sourceMapResult.getPath());
121                        // options.setSourceMapLocationMappings(Lists.newArrayList(new
122                        // SourceMap.LocationMapping(sourceDir.getPath() + File.separator, "")));
123                    }
124
125                    if (closureConfig.getWarningLevels() != null) {
126                        for (Map.Entry<DiagnosticGroup, CheckLevel> warningLevel : closureConfig.getWarningLevels().entrySet()) {
127                            options.setWarningLevel(warningLevel.getKey(), warningLevel.getValue());
128                        }
129                    }
130
131                    SourceFile input = SourceFile.fromInputStream(mergedFile.getName(), in, charset);
132                    List<SourceFile> externs = new ArrayList<>();
133                    externs.addAll(CommandLineRunner.getBuiltinExterns(closureConfig.getEnvironment()));
134                    externs.addAll(closureConfig.getExterns());
135
136                    Compiler compiler = new Compiler();
137                    compiler.compile(externs, Lists.newArrayList(input), options);
138
139                    // Check for errors.
140                    JSError[] errors = compiler.getErrors();
141                    if (errors.length > 0) {
142                        StringBuilder msg = new StringBuilder("JSCompiler errors\n");
143                        MessageFormatter formatter = new LightweightMessageFormatter(compiler);
144                        for (JSError e : errors) {
145                            msg.append(formatter.formatError(e));
146                        }
147                        throw new RuntimeException(msg.toString());
148                    }
149
150                    writer.append(compiler.toSource());
151
152                    if (closureConfig.getSourceMapFormat() != null) {
153                        log.info("Creating the minified file map ["
154                                + (verbose ? sourceMapResult.getPath() : sourceMapResult.getName()) + "].");
155
156                        if (sourceMapResult.createNewFile()) {
157                            flushSourceMap(sourceMapResult, minifiedFile.getName(), compiler.getSourceMap());
158
159                            writer.append(System.getProperty("line.separator"));
160                            writer.append("//# sourceMappingURL=").append(sourceMapResult.getName());
161                        }
162                    }
163
164                    break;
165                case YUI:
166                    log.debug("Using YUI Compressor engine.");
167
168                    JavaScriptCompressor compressor = new JavaScriptCompressor(reader, new JavaScriptErrorReporter(log,
169                            mergedFile.getName()));
170                    compressor.compress(writer, yuiConfig.getLineBreak(), yuiConfig.isMunge(), verbose,
171                            yuiConfig.isPreserveSemicolons(), yuiConfig.isDisableOptimizations());
172                    break;
173                default:
174                    log.warn("JavaScript engine not supported.");
175                    break;
176            }
177        } catch (IOException e) {
178            log.error(
179                    "Failed to compress the JavaScript file ["
180                            + (verbose ? mergedFile.getPath() : mergedFile.getName()) + "].", e);
181            throw e;
182        }
183
184        logCompressionGains(mergedFile, minifiedFile);
185    }
186
187    private void flushSourceMap(File sourceMapOutputFile, String minifyFileName, SourceMap sourceMap) {
188        try (FileWriter out = new FileWriter(sourceMapOutputFile)) {
189            sourceMap.appendTo(out, minifyFileName);
190        } catch (IOException e) {
191            log.error("Failed to write the JavaScript Source Map file ["
192                    + (verbose ? sourceMapOutputFile.getPath() : sourceMapOutputFile.getName()) + "].", e);
193        }
194    }
195}