-
Notifications
You must be signed in to change notification settings - Fork 0
/
JSMin.php
355 lines (322 loc) · 13.3 KB
/
JSMin.php
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
<?php
/**
* @package Resources
* @author Billy Visto
*/
namespace Gustavus\Resources;
use RuntimeException;
/**
* Class to minify javascript files using Google's closure compiler
*
* @package Resources
* @author Billy Visto
*/
class JSMin
{
/**
* Extension to put on our temporary file flag
*/
const TEMPORARY_FLAG_EXT = '.tmpFlag';
/**
* Directory where the file watcher is running so we can execute our compiler.jar file
*/
private static $stagingDir = '/cis/www-etc/lib/Gustavus/Resources/jsStaging/';
/**
* Flag to specify if we should save a temporary file or not
*
* @var boolean
*/
private static $saveTemporaryFile = true;
/**
* Location for all of our minified files
*/
public static $minifiedFolder = '/js/min/';
/**
* File to store our minify information in.
* This file contains an json_encoded associative array with keys of the basename and values of the modified time of the file when we created our minified file
*/
public static $minifyInfoFile = '.gacmin';
/**
* Request parameters to pass to Google's closure compiler
*
* @var array
*/
private static $minifyOptions = [
'language_in' => 'ECMASCRIPT5',
'compilation_level' => 'SIMPLE', // WHITESPACE_ONLY, SIMPLE, ADVANCED
];
/**
* Minify params that are allowed to be customized
*
* @var array
*/
private static $customizableOptions = [
'language_in',
'language_out',
'compilation_level',
];
/**
* Minifies a file and saves it with our minified extension.
*
* @param string $filePath Path to the file to minify
* @param array $options Additional options to use when minifying the file
* @return string|array Minified file path or array with results
*/
public static function minifyFile($filePath, Array $options = [])
{
if (strpos($filePath, self::$minifiedFolder) !== false) {
// this file appears to already be minified
return $filePath;
}
// add our doc root onto the file path
$filePath = Resource::addDocRootToPath($filePath);
if (!file_exists($filePath)) {
return self::removeDocRootFromPath($filePath);
}
// get our default options
$minifyOptions = self::$minifyOptions;
if (!empty($options)) {
// add our customized options to our default set
foreach ($options as $key => $value) {
if (in_array($key, self::$customizableOptions)) {
$minifyOptions[$key] = $value;
}
}
}
$baseDir = dirname($filePath) . '/';
$baseName = basename($filePath);
$minifiedBaseName = sprintf('%s-%s.js', str_replace('.js', '', $baseName), md5($baseDir));
$minifiedFilePath = Resource::addDocRootToPath(self::$minifiedFolder) . $minifiedBaseName;
// path to our info file
$minifyInfoPath = Resource::addDocRootToPath(self::$minifiedFolder) . self::$minifyInfoFile;
// Note: We use an info file because just comparing timestamps might not be enough in some situations. ie. If we removed a minified file on Lisa, then copied a file from Bart, the copied file on Lisa could still have an mtime less than that of our new minified file.
// build our options hash so we can determine if the file was minified with the same options
$minifyOptionsHash = self::buildMinifyOptionsHash($minifyOptions);
$fileMTime = filemtime($filePath);
if (file_exists($minifyInfoPath)) {
$minifyInfo = json_decode(file_get_contents($minifyInfoPath), true);
} else {
$minifyInfo = [];
}
// Look at our info file to
if (!empty($minifyInfo) && file_exists($minifiedFilePath) && filesize($minifiedFilePath) > 0) {
// we need to see when we last minified the file.
// make sure the correct information is in our info file
if (isset($minifyInfo[$minifiedBaseName], $minifyInfo[$minifiedBaseName]['optionsHash'], $minifyInfo[$minifiedBaseName]['mTime'])) {
$fileInfo = $minifyInfo[$minifiedBaseName];
if (!empty($options) && $fileInfo['optionsHash'] !== $minifyOptionsHash) {
// we have customized options that don't match the options the file was previously built with.
// Note: A file generated with the default options will overwrite a file generated with custom options. This error will get triggered the next time the custom options are used.
trigger_error(sprintf('It looks like the file: "%s" has already been minified with different options. Not minifying.', $filePath), E_USER_NOTICE);
return self::removeDocRootFromPath($filePath);
}
if ($fileInfo['mTime'] === $fileMTime) {
// the file we want to minify hasn't been modified since we last minified it
if (file_exists($minifiedFilePath . self::TEMPORARY_FLAG_EXT)) {
// our temporary flag exists
return [
'minPath' => self::removeDocRootFromPath($minifiedFilePath),
'temporary' => true,
];
}
return self::removeDocRootFromPath($minifiedFilePath);
}
}
}
// we need to save our modification time and options hash
$minifyInfo[$minifiedBaseName] = [
'optionsHash' => $minifyOptionsHash,
'mTime' => $fileMTime,
'sourceFile' => $filePath,
];
if (self::$saveTemporaryFile) {
// actually put a temporary file there to have something in place while waiting for the stagedFile to be ran.
$temporaryFile = self::buildTemporaryFile($filePath);
if ((file_exists($minifiedFilePath) && !is_writable($minifiedFilePath)) || !is_writable(Resource::addDocRootToPath(self::$minifiedFolder))) {
// we can't write to our minified file
trigger_error(sprintf('Couldn\'t write to the file: "%s"', $minifiedFilePath), E_USER_NOTICE);
return self::removeDocRootFromPath($filePath);
}
if ((file_exists($minifiedFilePath . self::TEMPORARY_FLAG_EXT) && !is_writable($minifiedFilePath . self::TEMPORARY_FLAG_EXT)) || !is_writable(Resource::addDocRootToPath(self::$minifiedFolder))) {
// we can't save our temporary flag
trigger_error(sprintf('Couldn\'t save our temporary flag file: "%s"', $minifiedFilePath . self::TEMPORARY_FLAG_EXT), E_USER_NOTICE);
return self::removeDocRootFromPath($filePath);
}
file_put_contents($minifiedFilePath, $temporaryFile);
file_put_contents($minifiedFilePath . self::TEMPORARY_FLAG_EXT, 'temporary flag');
}
if (!self::stageFile($filePath, $minifiedFilePath, $minifyOptions)) {
return self::removeDocRootFromPath($filePath);
}
if ((file_exists($minifyInfoPath) && !is_writable($minifyInfoPath)) || !is_writable(Resource::addDocRootToPath(self::$minifiedFolder))) {
// our info file is not writable
trigger_error(sprintf('Couldn\'t write to our minify info file: "%s"', $minifyInfoPath), E_USER_NOTICE);
return self::removeDocRootFromPath($filePath);
}
file_put_contents($minifyInfoPath, json_encode($minifyInfo));
if (self::$saveTemporaryFile) {
return [
'minPath' => self::removeDocRootFromPath($minifiedFilePath),
'temporary' => true,
];
}
return self::removeDocRootFromPath($minifiedFilePath);
}
/**
* Bundles javascript resources together into one file.
*
* @param array $resourcePaths Paths to all the resources we are bundling
* @param array $resourceMTimes Modification times of all the files we are bundling
* @param boolean $minify Whether we want these to be minified or just bundled
* @return string Path to the bundled resource
*/
public static function bundle(array $resourcePaths, array $resourceMTimes, $minify = true)
{
$bundleName = sprintf('%sBNDL-%s.js', str_replace('.js', '', basename(end($resourcePaths))), md5(implode(',', $resourcePaths)));
$bundlePath = sprintf('%s%s', self::$minifiedFolder, $bundleName);
$absBundlePath = Resource::addDocRootToPath($bundlePath);
if ((file_exists($absBundlePath) && !is_writable($absBundlePath)) || !is_writable(Resource::addDocRootToPath(self::$minifiedFolder))) {
// we can't write to our minified file
throw new RuntimeException(sprintf('Couldn\'t write to the file: "%s"', $absBundlePath));
}
$minifyInfoPath = Resource::addDocRootToPath(self::$minifiedFolder) . self::$minifyInfoFile;
if (file_exists($minifyInfoPath)) {
$minifyInfo = json_decode(file_get_contents($minifyInfoPath), true);
} else {
$minifyInfo = [];
}
if (!empty($minifyInfo) &&
file_exists($absBundlePath) &&
filesize($absBundlePath) > 0 &&
isset($minifyInfo[$bundleName], $minifyInfo[$bundleName]['mTimes']) &&
$minifyInfo[$bundleName]['mTimes'] === json_encode($resourceMTimes)) {
// our file hasn't changed.
return $bundlePath;
}
if (!$minify) {
// we need to manually build our bundle from the non-minified resources.
$bundle = '';
$joinSeparator = '';
foreach ($resourcePaths as $resourcePath) {
$resourcePath = Resource::addDocRootToPath($resourcePath);
if (!file_exists($resourcePath)) {
continue;
}
$bundle .= $joinSeparator . file_get_contents($resourcePath);
$joinSeparator = ';';
}
} else {
$bundle = self::buildTemporaryFile($resourcePaths);
$absResourcePaths = array_map(function($item) {return Resource::addDocRootToPath($item);}, $resourcePaths);
if (!self::stageFile($absResourcePaths, $absBundlePath, self::$minifyOptions)) {
trigger_error(sprintf('Failed to stage the file: %s', $bundleName), E_USER_NOTICE);
}
}
$minifyInfo[$bundleName] = [
'mTimes' => json_encode($resourceMTimes),
'sourceFiles' => json_encode($resourcePaths),
];
if ((file_exists($minifyInfoPath) && !is_writable($minifyInfoPath)) || !is_writable(Resource::addDocRootToPath(self::$minifiedFolder))) {
// our info file is not writable
trigger_error(sprintf('Couldn\'t write to our minify info file: "%s"', $minifyInfoPath), E_USER_NOTICE);
} else {
file_put_contents($minifyInfoPath, json_encode($minifyInfo));
}
file_put_contents($absBundlePath, $bundle);
file_put_contents($absBundlePath . self::TEMPORARY_FLAG_EXT, 'temporary flag');
return $bundlePath;
}
/**
* Builds a temporary file using our old minifier
*
* @param string $filePath Path to the file to build
* @return string
*/
private static function buildTemporaryFile($filePath)
{
if (is_array($filePath)) {
$path = '';
foreach ($filePath as $file) {
$path .= ',' . self::removeDocRootFromPath($file);
}
$path = ltrim($path, ',');
} else {
$path = self::removeDocRootFromPath($filePath);
}
$_GET['f'] = $path;
$min_serveOptions['quiet'] = true;
$min_serveOptions['encodeOutput'] = false;
$minifyResult = include '/cis/www/min/index.php';
return $minifyResult['content'];
}
/**
* Saves a file with the compiler command to be executed in our watched directory
*
* @param string $sourceFilePath Source file to compile
* @param string $destinationPath Destination of the compiled file
* @param Array $compilationOptions Options to pass to the compiler
* @return void
*/
private static function stageFile($sourceFilePath, $destinationPath, Array $compilationOptions)
{
if (!is_dir(self::$stagingDir)) {
mkdir(self::$stagingDir, 0777, true);
}
if (is_array($sourceFilePath)) {
$sourceFilePath = implode(' ', $sourceFilePath);
$isBundle = true;
} else {
$isBundle = false;
}
$options = '';
foreach ($compilationOptions as $option => $value) {
$options .= sprintf(' --%s %s', $option, $value);
}
if ($isBundle || !self::reportWarningsForFile($sourceFilePath)) {
// don't output warnings
$options .= ' --warning_level QUIET';
}
$cmd = sprintf('java -jar /cis/lib/Gustavus/Resources/closure-compiler/compiler.jar --js_output_file %s%s %s', $destinationPath, $options, $sourceFilePath);
file_put_contents(self::$stagingDir . basename($destinationPath), $cmd);
return true;
}
/**
* Checks whether we want to report warnings for the specified file.
*
* @param string $filePath Absolute path to the file in question
* @return boolean
*/
private static function reportWarningsForFile($filePath)
{
if (preg_match(sprintf('`%s/+js/Gustavus`', rtrim($_SERVER['DOCUMENT_ROOT'], '/')), $filePath)) {
// we want our Gustavus utiltiy bundle to throw warnings
return true;
} else if (preg_match(sprintf('`%s/+js/`', rtrim($_SERVER['DOCUMENT_ROOT'], '/')), $filePath)) {
// we don't want our third party libraries to throw warnings
return false;
}
// @todo add a blacklist if we need more to be excluded
// everything else defaults to throwing warnings
return true;
}
/**
* Builds a hash from our request parameters
*
* @param array $options Minification options
* @return string
*/
private static function buildMinifyOptionsHash($options)
{
return md5(json_encode($options));
}
/**
* Removes the DOC_ROOT from the filepath
*
* @param string $filePath Path of the file to remove the doc root for
* @return string
*/
private static function removeDocRootFromPath($filePath)
{
return str_replace('//', '/', '/' . str_replace($_SERVER['DOCUMENT_ROOT'], '', $filePath));
}
}