/
JsExtension.java
176 lines (149 loc) · 7.24 KB
/
JsExtension.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xpn.xwiki.web.sx;
import java.io.IOException;
import java.util.Collections;
import javax.inject.Provider;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.skinx.SkinExtensionConfiguration;
import com.google.javascript.jscomp.CompilationLevel;
import com.google.javascript.jscomp.Compiler;
import com.google.javascript.jscomp.CompilerOptions;
import com.google.javascript.jscomp.CompilerOptions.LanguageMode;
import com.google.javascript.jscomp.Result;
import com.google.javascript.jscomp.SourceFile;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.web.Utils;
/**
* JavaScript extension.
*
* @version $Id$
* @since 1.7M2
*/
public class JsExtension implements Extension
{
/** Logging helper. */
private static final Logger LOGGER = LoggerFactory.getLogger(JsExtension.class);
@Override
public String getClassName()
{
return "XWiki.JavaScriptExtension";
}
@Override
public String getContentType()
{
return "text/javascript; charset=UTF-8";
}
@Override
public SxCompressor getCompressor()
{
return new JsCompressor();
}
/**
* The JavaScript compressor returned by {@link JsExtension#getCompressor()}. Currently implemented using Closure
* Compiler.
*/
public static class JsCompressor implements SxCompressor
{
private String sourceMap;
@Override
public String compress(String source)
{
Compiler compiler = new Compiler();
// Configure the Closure Compiler. We should use as much as possible the same configuration we use for
// minifying JavaScript code at build time (with the corresponding Maven plugin).
CompilerOptions options = new CompilerOptions();
// Support the latest stable ECMAScript features (excludes drafts) as input.
options.setLanguageIn(LanguageMode.STABLE);
// The output language must match the highest ECMAScript version supported by all the browsers we support.
// See https://dev.xwiki.org/xwiki/bin/view/Community/SupportStrategy/BrowserSupportStrategy
// As long as we support IE11 we need to output a lower version of ECMAScript.
options.setLanguageOut(LanguageMode.ECMASCRIPT5_STRICT);
// Enable the JavaScript strict mode based on the configuration.
options.setStrictModeInput(getConfig().shouldRunJavaScriptInStrictMode());
options.setEmitUseStrict(options.expectStrictModeInput());
// Add support for using the latest JavaScript APIs by including the necessary polyfills in the output. Note
// that the polyfills won't be included if the JavaScript minification is disabled (e.g. from the debug
// configuration) so running the unminified code on browsers that don't support the latest APIs (e.g. IE11)
// won't work, if you use such APIs.
options.setRewritePolyfills(true);
// Generate the source map so that we have meaningful error messages. The specified path is not used. We
// just need to set a path in order to enable source map generation.
options.setSourceMapOutputPath(getSourceFileName() + ".map");
// Do some simple optimizations, besides removing the whitespace and renaming local variables. This includes
// removing dead code and generating warnings that can help us improve the JavaScript code.
CompilationLevel.SIMPLE_OPTIMIZATIONS.setOptionsForCompilationLevel(options);
// We don't have access to the name of the JavaScript extension that is being compressed so we use a fake
// name that will be referenced in the generated warning and error messages.
SourceFile input = SourceFile.fromCode(getSourceFileName(), source);
// Build the syntax tree from the source JavaScript.
Result result = compiler.compile(Collections.emptyList(), Collections.singletonList(input), options);
// Log the warning and the errors that occurred.
compiler.getWarnings().forEach(warning -> LOGGER.warn("Warning at line [{}], column [{}]: [{}]",
warning.getLineNumber(), warning.getCharno(), warning.getDescription()));
compiler.getErrors().forEach(error -> LOGGER.error("Error at line [{}], column [{}]: [{}]",
error.getLineNumber(), error.getCharno(), error.getDescription()));
if (result.success) {
try {
// Generate the compressed JavaScript code and its source map.
String compressed = compiler.toSource();
// Store the source map to be used later. We have to do this after generating the compressed code
// because otherwise the source map is empty.
this.sourceMap = getSourceMap(compiler);
return compressed;
} catch (Exception e) {
LOGGER.warn("Failed to compress JavaScript extension. Root cause is: [{}].",
ExceptionUtils.getRootCauseMessage(e));
}
}
// Fall-back on the original source if the compression failed.
return source;
}
private String getSourceFileName()
{
// Return the reference of the current document on the XWiki context.
Provider<XWikiContext> xcontextProvider = Utils.getComponent(XWikiContext.TYPE_PROVIDER);
return xcontextProvider.get().getDoc().getDocumentReference().toString();
}
private String getSourceMap(Compiler compiler) throws IOException
{
if (compiler.getSourceMap() != null) {
StringBuilder sourceMapping = new StringBuilder();
compiler.getSourceMap().appendTo(sourceMapping, getSourceFileName());
return sourceMapping.toString();
} else {
return null;
}
}
/**
* @return the last source map that was created by this compressor
*/
public String getSourceMap()
{
return this.sourceMap;
}
private SkinExtensionConfiguration getConfig()
{
return Utils.getComponent(SkinExtensionConfiguration.class);
}
}
}