1 | <?php |
||
2 | /** |
||
3 | * @link https://www.yiiframework.com/ |
||
4 | * @copyright Copyright (c) 2008 Yii Software LLC |
||
5 | * @license https://www.yiiframework.com/license/ |
||
6 | */ |
||
7 | |||
8 | namespace yii\console\controllers; |
||
9 | |||
10 | use Yii; |
||
11 | use yii\console\Controller; |
||
12 | use yii\console\Exception; |
||
13 | use yii\console\ExitCode; |
||
14 | use yii\helpers\Console; |
||
15 | use yii\helpers\FileHelper; |
||
16 | use yii\helpers\VarDumper; |
||
17 | use yii\web\AssetBundle; |
||
18 | |||
19 | /** |
||
20 | * Allows you to combine and compress your JavaScript and CSS files. |
||
21 | * |
||
22 | * Usage: |
||
23 | * |
||
24 | * 1. Create a configuration file using the `template` action: |
||
25 | * |
||
26 | * yii asset/template /path/to/myapp/config.php |
||
27 | * |
||
28 | * 2. Edit the created config file, adjusting it for your web application needs. |
||
29 | * 3. Run the 'compress' action, using created config: |
||
30 | * |
||
31 | * yii asset /path/to/myapp/config.php /path/to/myapp/config/assets_compressed.php |
||
32 | * |
||
33 | * 4. Adjust your web application config to use compressed assets. |
||
34 | * |
||
35 | * Note: in the console environment some [path aliases](guide:concept-aliases) like `@webroot` and `@web` may not exist, |
||
36 | * so corresponding paths inside the configuration should be specified directly. |
||
37 | * |
||
38 | * Note: by default this command relies on an external tools to perform actual files compression, |
||
39 | * check [[jsCompressor]] and [[cssCompressor]] for more details. |
||
40 | * |
||
41 | * @property \yii\web\AssetManager $assetManager Asset manager instance. Note that the type of this property |
||
42 | * differs in getter and setter. See [[getAssetManager()]] and [[setAssetManager()]] for details. |
||
43 | * |
||
44 | * @author Qiang Xue <[email protected]> |
||
45 | * @author Paul Klimov <[email protected]> |
||
46 | * @since 2.0 |
||
47 | */ |
||
48 | class AssetController extends Controller |
||
49 | { |
||
50 | /** |
||
51 | * @var string controller default action ID. |
||
52 | */ |
||
53 | public $defaultAction = 'compress'; |
||
54 | /** |
||
55 | * @var array list of asset bundles to be compressed. |
||
56 | */ |
||
57 | public $bundles = []; |
||
58 | /** |
||
59 | * @var array list of asset bundles, which represents output compressed files. |
||
60 | * You can specify the name of the output compressed file using 'css' and 'js' keys: |
||
61 | * For example: |
||
62 | * |
||
63 | * ```php |
||
64 | * 'app\config\AllAsset' => [ |
||
65 | * 'js' => 'js/all-{hash}.js', |
||
66 | * 'css' => 'css/all-{hash}.css', |
||
67 | * 'depends' => [ ... ], |
||
68 | * ] |
||
69 | * ``` |
||
70 | * |
||
71 | * File names can contain placeholder "{hash}", which will be filled by the hash of the resulting file. |
||
72 | * |
||
73 | * You may specify several target bundles in order to compress different groups of assets. |
||
74 | * In this case you should use 'depends' key to specify, which bundles should be covered with particular |
||
75 | * target bundle. You may leave 'depends' to be empty for single bundle, which will compress all remaining |
||
76 | * bundles in this case. |
||
77 | * For example: |
||
78 | * |
||
79 | * ```php |
||
80 | * 'allShared' => [ |
||
81 | * 'js' => 'js/all-shared-{hash}.js', |
||
82 | * 'css' => 'css/all-shared-{hash}.css', |
||
83 | * 'depends' => [ |
||
84 | * // Include all assets shared between 'backend' and 'frontend' |
||
85 | * 'yii\web\YiiAsset', |
||
86 | * 'app\assets\SharedAsset', |
||
87 | * ], |
||
88 | * ], |
||
89 | * 'allBackEnd' => [ |
||
90 | * 'js' => 'js/all-{hash}.js', |
||
91 | * 'css' => 'css/all-{hash}.css', |
||
92 | * 'depends' => [ |
||
93 | * // Include only 'backend' assets: |
||
94 | * 'app\assets\AdminAsset' |
||
95 | * ], |
||
96 | * ], |
||
97 | * 'allFrontEnd' => [ |
||
98 | * 'js' => 'js/all-{hash}.js', |
||
99 | * 'css' => 'css/all-{hash}.css', |
||
100 | * 'depends' => [], // Include all remaining assets |
||
101 | * ], |
||
102 | * ``` |
||
103 | */ |
||
104 | public $targets = []; |
||
105 | /** |
||
106 | * @var string|callable JavaScript file compressor. |
||
107 | * If a string, it is treated as shell command template, which should contain |
||
108 | * placeholders {from} - source file name - and {to} - output file name. |
||
109 | * Otherwise, it is treated as PHP callback, which should perform the compression. |
||
110 | * |
||
111 | * Default value relies on usage of "Closure Compiler" |
||
112 | * @see https://developers.google.com/closure/compiler/ |
||
113 | */ |
||
114 | public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; |
||
115 | /** |
||
116 | * @var string|callable CSS file compressor. |
||
117 | * If a string, it is treated as shell command template, which should contain |
||
118 | * placeholders {from} - source file name - and {to} - output file name. |
||
119 | * Otherwise, it is treated as PHP callback, which should perform the compression. |
||
120 | * |
||
121 | * Default value relies on usage of "YUI Compressor" |
||
122 | * @see https://github.com/yui/yuicompressor/ |
||
123 | */ |
||
124 | public $cssCompressor = 'java -jar yuicompressor.jar --type css {from} -o {to}'; |
||
125 | /** |
||
126 | * @var bool whether to delete asset source files after compression. |
||
127 | * This option affects only those bundles, which have [[\yii\web\AssetBundle::sourcePath]] is set. |
||
128 | * @since 2.0.10 |
||
129 | */ |
||
130 | public $deleteSource = false; |
||
131 | |||
132 | /** |
||
133 | * @var array|\yii\web\AssetManager [[\yii\web\AssetManager]] instance or its array configuration, which will be used |
||
134 | * for assets processing. |
||
135 | */ |
||
136 | private $_assetManager = []; |
||
137 | |||
138 | |||
139 | /** |
||
140 | * Returns the asset manager instance. |
||
141 | * @throws \yii\console\Exception on invalid configuration. |
||
142 | * @return \yii\web\AssetManager asset manager instance. |
||
143 | */ |
||
144 | 5 | public function getAssetManager() |
|
145 | { |
||
146 | 5 | if (!is_object($this->_assetManager)) { |
|
147 | 5 | $options = $this->_assetManager; |
|
148 | 5 | if (!isset($options['class'])) { |
|
149 | 5 | $options['class'] = 'yii\\web\\AssetManager'; |
|
150 | } |
||
151 | 5 | if (!isset($options['basePath'])) { |
|
152 | throw new Exception("Please specify 'basePath' for the 'assetManager' option."); |
||
153 | } |
||
154 | 5 | if (!isset($options['baseUrl'])) { |
|
155 | throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); |
||
156 | } |
||
157 | |||
158 | 5 | if (!isset($options['forceCopy'])) { |
|
159 | 5 | $options['forceCopy'] = true; |
|
160 | } |
||
161 | |||
162 | 5 | $this->_assetManager = Yii::createObject($options); |
|
163 | } |
||
164 | |||
165 | 5 | return $this->_assetManager; |
|
166 | } |
||
167 | |||
168 | /** |
||
169 | * Sets asset manager instance or configuration. |
||
170 | * @param \yii\web\AssetManager|array $assetManager asset manager instance or its array configuration. |
||
171 | * @throws \yii\console\Exception on invalid argument type. |
||
172 | */ |
||
173 | 5 | public function setAssetManager($assetManager) |
|
174 | { |
||
175 | 5 | if (is_scalar($assetManager)) { |
|
176 | throw new Exception('"' . get_class($this) . '::assetManager" should be either object or array - "' . gettype($assetManager) . '" given.'); |
||
177 | } |
||
178 | 5 | $this->_assetManager = $assetManager; |
|
179 | } |
||
180 | |||
181 | /** |
||
182 | * Combines and compresses the asset files according to the given configuration. |
||
183 | * During the process new asset bundle configuration file will be created. |
||
184 | * You should replace your original asset bundle configuration with this file in order to use compressed files. |
||
185 | * @param string $configFile configuration file name. |
||
186 | * @param string $bundleFile output asset bundles configuration file name. |
||
187 | */ |
||
188 | 5 | public function actionCompress($configFile, $bundleFile) |
|
189 | { |
||
190 | 5 | $this->loadConfiguration($configFile); |
|
191 | 5 | $bundles = $this->loadBundles($this->bundles); |
|
192 | 4 | $targets = $this->loadTargets($this->targets, $bundles); |
|
193 | 4 | foreach ($targets as $name => $target) { |
|
194 | 4 | $this->stdout("Creating output bundle '{$name}':\n"); |
|
195 | 4 | if (!empty($target->js)) { |
|
196 | 4 | $this->buildTarget($target, 'js', $bundles); |
|
197 | } |
||
198 | 4 | if (!empty($target->css)) { |
|
199 | 4 | $this->buildTarget($target, 'css', $bundles); |
|
200 | } |
||
201 | 4 | $this->stdout("\n"); |
|
202 | } |
||
203 | |||
204 | 4 | $targets = $this->adjustDependency($targets, $bundles); |
|
205 | 4 | $this->saveTargets($targets, $bundleFile); |
|
206 | |||
207 | 4 | if ($this->deleteSource) { |
|
208 | 1 | $this->deletePublishedAssets($bundles); |
|
209 | } |
||
210 | } |
||
211 | |||
212 | /** |
||
213 | * Applies configuration from the given file to self instance. |
||
214 | * @param string $configFile configuration file name. |
||
215 | * @throws \yii\console\Exception on failure. |
||
216 | */ |
||
217 | 5 | protected function loadConfiguration($configFile) |
|
218 | { |
||
219 | 5 | $this->stdout("Loading configuration from '{$configFile}'...\n"); |
|
220 | 5 | $config = require $configFile; |
|
221 | 5 | foreach ($config as $name => $value) { |
|
222 | 5 | if (property_exists($this, $name) || $this->canSetProperty($name)) { |
|
223 | 5 | $this->$name = $value; |
|
224 | } else { |
||
225 | throw new Exception("Unknown configuration option: $name"); |
||
226 | } |
||
227 | } |
||
228 | |||
229 | 5 | $this->getAssetManager(); // check if asset manager configuration is correct |
|
230 | } |
||
231 | |||
232 | /** |
||
233 | * Creates full list of source asset bundles. |
||
234 | * @param string[] $bundles list of asset bundle names |
||
235 | * @return \yii\web\AssetBundle[] list of source asset bundles. |
||
236 | */ |
||
237 | 5 | protected function loadBundles($bundles) |
|
238 | { |
||
239 | 5 | $this->stdout("Collecting source bundles information...\n"); |
|
240 | |||
241 | 5 | $am = $this->getAssetManager(); |
|
242 | 5 | $result = []; |
|
243 | 5 | foreach ($bundles as $name) { |
|
244 | 5 | $result[$name] = $am->getBundle($name); |
|
245 | } |
||
246 | 5 | foreach ($result as $bundle) { |
|
247 | 5 | $this->loadDependency($bundle, $result); |
|
248 | } |
||
249 | |||
250 | 4 | return $result; |
|
251 | } |
||
252 | |||
253 | /** |
||
254 | * Loads asset bundle dependencies recursively. |
||
255 | * @param \yii\web\AssetBundle $bundle bundle instance |
||
256 | * @param array $result already loaded bundles list. |
||
257 | * @throws Exception on failure. |
||
258 | */ |
||
259 | 5 | protected function loadDependency($bundle, &$result) |
|
260 | { |
||
261 | 5 | $am = $this->getAssetManager(); |
|
262 | 5 | foreach ($bundle->depends as $name) { |
|
263 | 2 | if (!isset($result[$name])) { |
|
264 | 2 | $dependencyBundle = $am->getBundle($name); |
|
265 | 2 | $result[$name] = false; |
|
266 | 2 | $this->loadDependency($dependencyBundle, $result); |
|
267 | 1 | $result[$name] = $dependencyBundle; |
|
268 | 1 | } elseif ($result[$name] === false) { |
|
269 | 1 | throw new Exception("A circular dependency is detected for bundle '{$name}': " . $this->composeCircularDependencyTrace($name, $result) . '.'); |
|
270 | } |
||
271 | } |
||
272 | } |
||
273 | |||
274 | /** |
||
275 | * Creates full list of output asset bundles. |
||
276 | * @param array $targets output asset bundles configuration. |
||
277 | * @param \yii\web\AssetBundle[] $bundles list of source asset bundles. |
||
278 | * @return \yii\web\AssetBundle[] list of output asset bundles. |
||
279 | * @throws Exception on failure. |
||
280 | */ |
||
281 | 4 | protected function loadTargets($targets, $bundles) |
|
282 | { |
||
283 | // build the dependency order of bundles |
||
284 | 4 | $registered = []; |
|
285 | 4 | foreach ($bundles as $name => $bundle) { |
|
286 | 4 | $this->registerBundle($bundles, $name, $registered); |
|
287 | } |
||
288 | 4 | $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); |
|
289 | |||
290 | // fill up the target which has empty 'depends'. |
||
291 | 4 | $referenced = []; |
|
292 | 4 | foreach ($targets as $name => $target) { |
|
293 | 4 | if (empty($target['depends'])) { |
|
294 | 4 | if (!isset($all)) { |
|
295 | 4 | $all = $name; |
|
296 | } else { |
||
297 | 4 | throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name"); |
|
298 | } |
||
299 | } else { |
||
300 | foreach ($target['depends'] as $bundle) { |
||
301 | if (!isset($referenced[$bundle])) { |
||
302 | $referenced[$bundle] = $name; |
||
303 | } else { |
||
304 | throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time."); |
||
305 | } |
||
306 | } |
||
307 | } |
||
308 | } |
||
309 | 4 | if (isset($all)) { |
|
310 | 4 | $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced)); |
|
311 | } |
||
312 | |||
313 | // adjust the 'depends' order for each target according to the dependency order of bundles |
||
314 | // create an AssetBundle object for each target |
||
315 | 4 | foreach ($targets as $name => $target) { |
|
316 | 4 | if (!isset($target['basePath'])) { |
|
317 | throw new Exception("Please specify 'basePath' for the '$name' target."); |
||
318 | } |
||
319 | 4 | if (!isset($target['baseUrl'])) { |
|
320 | throw new Exception("Please specify 'baseUrl' for the '$name' target."); |
||
321 | } |
||
322 | 4 | usort($target['depends'], function ($a, $b) use ($bundleOrders) { |
|
323 | 1 | if ($bundleOrders[$a] == $bundleOrders[$b]) { |
|
324 | return 0; |
||
325 | } |
||
326 | |||
327 | 1 | return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; |
|
328 | 4 | }); |
|
329 | 4 | if (!isset($target['class'])) { |
|
330 | 4 | $target['class'] = $name; |
|
331 | } |
||
332 | 4 | $targets[$name] = Yii::createObject($target); |
|
333 | } |
||
334 | |||
335 | 4 | return $targets; |
|
336 | } |
||
337 | |||
338 | /** |
||
339 | * Builds output asset bundle. |
||
340 | * @param \yii\web\AssetBundle $target output asset bundle |
||
341 | * @param string $type either 'js' or 'css'. |
||
342 | * @param \yii\web\AssetBundle[] $bundles source asset bundles. |
||
343 | * @throws Exception on failure. |
||
344 | */ |
||
345 | 4 | protected function buildTarget($target, $type, $bundles) |
|
346 | { |
||
347 | 4 | $inputFiles = []; |
|
348 | 4 | foreach ($target->depends as $name) { |
|
349 | 4 | if (isset($bundles[$name])) { |
|
350 | 4 | if (!$this->isBundleExternal($bundles[$name])) { |
|
351 | 4 | foreach ($bundles[$name]->$type as $file) { |
|
352 | 3 | if (is_array($file)) { |
|
353 | $inputFiles[] = $bundles[$name]->basePath . '/' . $file[0]; |
||
354 | } else { |
||
355 | 3 | $inputFiles[] = $bundles[$name]->basePath . '/' . $file; |
|
356 | } |
||
357 | } |
||
358 | } |
||
359 | } else { |
||
360 | throw new Exception("Unknown bundle: '{$name}'"); |
||
361 | } |
||
362 | } |
||
363 | |||
364 | 4 | if (empty($inputFiles)) { |
|
365 | 1 | $target->$type = []; |
|
366 | } else { |
||
367 | 3 | FileHelper::createDirectory($target->basePath, $this->getAssetManager()->dirMode); |
|
368 | 3 | $tempFile = $target->basePath . '/' . strtr($target->$type, ['{hash}' => 'temp']); |
|
369 | |||
370 | 3 | if ($type === 'js') { |
|
371 | 3 | $this->compressJsFiles($inputFiles, $tempFile); |
|
372 | } else { |
||
373 | 3 | $this->compressCssFiles($inputFiles, $tempFile); |
|
374 | } |
||
375 | |||
376 | 3 | $targetFile = strtr($target->$type, ['{hash}' => md5_file($tempFile)]); |
|
377 | 3 | $outputFile = $target->basePath . '/' . $targetFile; |
|
378 | 3 | rename($tempFile, $outputFile); |
|
379 | 3 | $target->$type = [$targetFile]; |
|
380 | } |
||
381 | } |
||
382 | |||
383 | /** |
||
384 | * Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones. |
||
385 | * @param \yii\web\AssetBundle[] $targets output asset bundles. |
||
386 | * @param \yii\web\AssetBundle[] $bundles source asset bundles. |
||
387 | * @return \yii\web\AssetBundle[] output asset bundles. |
||
388 | */ |
||
389 | 4 | protected function adjustDependency($targets, $bundles) |
|
390 | { |
||
391 | 4 | $this->stdout("Creating new bundle configuration...\n"); |
|
392 | |||
393 | 4 | $map = []; |
|
394 | 4 | foreach ($targets as $name => $target) { |
|
395 | 4 | foreach ($target->depends as $bundle) { |
|
396 | 4 | $map[$bundle] = $name; |
|
397 | } |
||
398 | } |
||
399 | |||
400 | 4 | foreach ($targets as $name => $target) { |
|
401 | 4 | $depends = []; |
|
402 | 4 | foreach ($target->depends as $bn) { |
|
403 | 4 | foreach ($bundles[$bn]->depends as $bundle) { |
|
404 | 1 | $depends[$map[$bundle]] = true; |
|
405 | } |
||
406 | } |
||
407 | 4 | unset($depends[$name]); |
|
408 | 4 | $target->depends = array_keys($depends); |
|
409 | } |
||
410 | |||
411 | // detect possible circular dependencies |
||
412 | 4 | foreach ($targets as $name => $target) { |
|
413 | 4 | $registered = []; |
|
414 | 4 | $this->registerBundle($targets, $name, $registered); |
|
415 | } |
||
416 | |||
417 | 4 | foreach ($map as $bundle => $target) { |
|
418 | 4 | $sourceBundle = $bundles[$bundle]; |
|
419 | 4 | $depends = $sourceBundle->depends; |
|
420 | 4 | if (!$this->isBundleExternal($sourceBundle)) { |
|
421 | 3 | $depends[] = $target; |
|
422 | } |
||
423 | 4 | $targetBundle = clone $sourceBundle; |
|
424 | 4 | $targetBundle->depends = $depends; |
|
425 | 4 | $targets[$bundle] = $targetBundle; |
|
426 | } |
||
427 | |||
428 | 4 | return $targets; |
|
429 | } |
||
430 | |||
431 | /** |
||
432 | * Registers asset bundles including their dependencies. |
||
433 | * @param \yii\web\AssetBundle[] $bundles asset bundles list. |
||
434 | * @param string $name bundle name. |
||
435 | * @param array $registered stores already registered names. |
||
436 | * @throws Exception if circular dependency is detected. |
||
437 | */ |
||
438 | 4 | protected function registerBundle($bundles, $name, &$registered) |
|
439 | { |
||
440 | 4 | if (!isset($registered[$name])) { |
|
441 | 4 | $registered[$name] = false; |
|
442 | 4 | $bundle = $bundles[$name]; |
|
443 | 4 | foreach ($bundle->depends as $depend) { |
|
444 | 1 | $this->registerBundle($bundles, $depend, $registered); |
|
445 | } |
||
446 | 4 | unset($registered[$name]); |
|
447 | 4 | $registered[$name] = $bundle; |
|
448 | 1 | } elseif ($registered[$name] === false) { |
|
449 | throw new Exception("A circular dependency is detected for target '{$name}': " . $this->composeCircularDependencyTrace($name, $registered) . '.'); |
||
450 | } |
||
451 | } |
||
452 | |||
453 | /** |
||
454 | * Saves new asset bundles configuration. |
||
455 | * @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved. |
||
456 | * @param string $bundleFile output file name. |
||
457 | * @throws \yii\console\Exception on failure. |
||
458 | */ |
||
459 | 4 | protected function saveTargets($targets, $bundleFile) |
|
460 | { |
||
461 | 4 | $array = []; |
|
462 | 4 | foreach ($targets as $name => $target) { |
|
463 | 4 | if (isset($this->targets[$name])) { |
|
464 | 4 | $array[$name] = array_merge($this->targets[$name], [ |
|
465 | 4 | 'class' => get_class($target), |
|
466 | 4 | 'sourcePath' => null, |
|
467 | 4 | 'basePath' => $this->targets[$name]['basePath'], |
|
468 | 4 | 'baseUrl' => $this->targets[$name]['baseUrl'], |
|
469 | 4 | 'js' => $target->js, |
|
470 | 4 | 'css' => $target->css, |
|
471 | 4 | 'depends' => [], |
|
472 | 4 | ]); |
|
473 | } else { |
||
474 | 4 | if ($this->isBundleExternal($target)) { |
|
475 | 2 | $array[$name] = $this->composeBundleConfig($target); |
|
476 | } else { |
||
477 | 3 | $array[$name] = [ |
|
478 | 3 | 'sourcePath' => null, |
|
479 | 3 | 'js' => [], |
|
480 | 3 | 'css' => [], |
|
481 | 3 | 'depends' => $target->depends, |
|
482 | 3 | ]; |
|
483 | } |
||
484 | } |
||
485 | } |
||
486 | 4 | $array = VarDumper::export($array); |
|
487 | 4 | $version = date('Y-m-d H:i:s'); |
|
488 | 4 | $bundleFileContent = <<<EOD |
|
489 | 4 | <?php |
|
490 | /** |
||
491 | 4 | * This file is generated by the "yii {$this->id}" command. |
|
492 | * DO NOT MODIFY THIS FILE DIRECTLY. |
||
493 | 4 | * @version {$version} |
|
494 | */ |
||
495 | 4 | return {$array}; |
|
496 | 4 | EOD; |
|
497 | 4 | if (!file_put_contents($bundleFile, $bundleFileContent, LOCK_EX)) { |
|
498 | throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'."); |
||
499 | } |
||
500 | 4 | $this->stdout("Output bundle configuration created at '{$bundleFile}'.\n", Console::FG_GREEN); |
|
501 | } |
||
502 | |||
503 | /** |
||
504 | * Compresses given JavaScript files and combines them into the single one. |
||
505 | * @param array $inputFiles list of source file names. |
||
506 | * @param string $outputFile output file name. |
||
507 | * @throws \yii\console\Exception on failure |
||
508 | */ |
||
509 | 3 | protected function compressJsFiles($inputFiles, $outputFile) |
|
510 | { |
||
511 | 3 | if (empty($inputFiles)) { |
|
512 | return; |
||
513 | } |
||
514 | 3 | $this->stdout(" Compressing JavaScript files...\n"); |
|
515 | 3 | if (is_string($this->jsCompressor)) { |
|
516 | 3 | $tmpFile = $outputFile . '.tmp'; |
|
517 | 3 | $this->combineJsFiles($inputFiles, $tmpFile); |
|
518 | 3 | $this->stdout((string)shell_exec(strtr($this->jsCompressor, [ |
|
519 | 3 | '{from}' => escapeshellarg($tmpFile), |
|
520 | 3 | '{to}' => escapeshellarg($outputFile), |
|
521 | 3 | ]))); |
|
522 | 3 | @unlink($tmpFile); |
|
523 | } else { |
||
524 | call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); |
||
525 | } |
||
526 | 3 | if (!file_exists($outputFile)) { |
|
527 | throw new Exception("Unable to compress JavaScript files into '{$outputFile}'."); |
||
528 | } |
||
529 | 3 | $this->stdout(" JavaScript files compressed into '{$outputFile}'.\n"); |
|
530 | } |
||
531 | |||
532 | /** |
||
533 | * Compresses given CSS files and combines them into the single one. |
||
534 | * @param array $inputFiles list of source file names. |
||
535 | * @param string $outputFile output file name. |
||
536 | * @throws \yii\console\Exception on failure |
||
537 | */ |
||
538 | 3 | protected function compressCssFiles($inputFiles, $outputFile) |
|
539 | { |
||
540 | 3 | if (empty($inputFiles)) { |
|
541 | return; |
||
542 | } |
||
543 | 3 | $this->stdout(" Compressing CSS files...\n"); |
|
544 | 3 | if (is_string($this->cssCompressor)) { |
|
545 | 3 | $tmpFile = $outputFile . '.tmp'; |
|
546 | 3 | $this->combineCssFiles($inputFiles, $tmpFile); |
|
547 | 3 | $this->stdout((string)shell_exec(strtr($this->cssCompressor, [ |
|
548 | 3 | '{from}' => escapeshellarg($tmpFile), |
|
549 | 3 | '{to}' => escapeshellarg($outputFile), |
|
550 | 3 | ]))); |
|
551 | 3 | @unlink($tmpFile); |
|
552 | } else { |
||
553 | call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); |
||
554 | } |
||
555 | 3 | if (!file_exists($outputFile)) { |
|
556 | throw new Exception("Unable to compress CSS files into '{$outputFile}'."); |
||
557 | } |
||
558 | 3 | $this->stdout(" CSS files compressed into '{$outputFile}'.\n"); |
|
559 | } |
||
560 | |||
561 | /** |
||
562 | * Combines JavaScript files into a single one. |
||
563 | * @param array $inputFiles source file names. |
||
564 | * @param string $outputFile output file name. |
||
565 | * @throws \yii\console\Exception on failure. |
||
566 | */ |
||
567 | 3 | public function combineJsFiles($inputFiles, $outputFile) |
|
568 | { |
||
569 | 3 | $content = ''; |
|
570 | 3 | foreach ($inputFiles as $file) { |
|
571 | // Add a semicolon to source code if trailing semicolon missing. |
||
572 | // Notice: It needs a new line before `;` to avoid affection of line comment. (// ...;) |
||
573 | 3 | $fileContent = rtrim(file_get_contents($file)); |
|
574 | 3 | if (substr($fileContent, -1) !== ';') { |
|
575 | 3 | $fileContent .= "\n;"; |
|
576 | } |
||
577 | 3 | $content .= "/*** BEGIN FILE: $file ***/\n" |
|
578 | 3 | . $fileContent . "\n" |
|
579 | 3 | . "/*** END FILE: $file ***/\n"; |
|
580 | } |
||
581 | 3 | if (!file_put_contents($outputFile, $content)) { |
|
582 | throw new Exception("Unable to write output JavaScript file '{$outputFile}'."); |
||
583 | } |
||
584 | } |
||
585 | |||
586 | /** |
||
587 | * Combines CSS files into a single one. |
||
588 | * @param array $inputFiles source file names. |
||
589 | * @param string $outputFile output file name. |
||
590 | * @throws \yii\console\Exception on failure. |
||
591 | */ |
||
592 | 3 | public function combineCssFiles($inputFiles, $outputFile) |
|
593 | { |
||
594 | 3 | $content = ''; |
|
595 | 3 | $outputFilePath = dirname($this->findRealPath($outputFile)); |
|
596 | 3 | foreach ($inputFiles as $file) { |
|
597 | 3 | $content .= "/*** BEGIN FILE: $file ***/\n" |
|
598 | 3 | . $this->adjustCssUrl(file_get_contents($file), dirname($this->findRealPath($file)), $outputFilePath) |
|
599 | 3 | . "/*** END FILE: $file ***/\n"; |
|
600 | } |
||
601 | 3 | if (!file_put_contents($outputFile, $content)) { |
|
602 | throw new Exception("Unable to write output CSS file '{$outputFile}'."); |
||
603 | } |
||
604 | } |
||
605 | |||
606 | /** |
||
607 | * Adjusts CSS content allowing URL references pointing to the original resources. |
||
608 | * @param string $cssContent source CSS content. |
||
609 | * @param string $inputFilePath input CSS file name. |
||
610 | * @param string $outputFilePath output CSS file name. |
||
611 | * @return string adjusted CSS content. |
||
612 | */ |
||
613 | 16 | protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath) |
|
614 | { |
||
615 | 16 | $inputFilePath = str_replace('\\', '/', $inputFilePath); |
|
616 | 16 | $outputFilePath = str_replace('\\', '/', $outputFilePath); |
|
617 | |||
618 | 16 | $sharedPathParts = []; |
|
619 | 16 | $inputFilePathParts = explode('/', $inputFilePath); |
|
620 | 16 | $inputFilePathPartsCount = count($inputFilePathParts); |
|
621 | 16 | $outputFilePathParts = explode('/', $outputFilePath); |
|
622 | 16 | $outputFilePathPartsCount = count($outputFilePathParts); |
|
623 | 16 | for ($i = 0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) { |
|
624 | 16 | if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) { |
|
625 | 16 | $sharedPathParts[] = $inputFilePathParts[$i]; |
|
626 | } else { |
||
627 | 13 | break; |
|
628 | } |
||
629 | } |
||
630 | 16 | $sharedPath = implode('/', $sharedPathParts); |
|
631 | |||
632 | 16 | $inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/'); |
|
633 | 16 | $outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/'); |
|
634 | 16 | if (empty($inputFileRelativePath)) { |
|
635 | 1 | $inputFileRelativePathParts = []; |
|
636 | } else { |
||
637 | 15 | $inputFileRelativePathParts = explode('/', $inputFileRelativePath); |
|
638 | } |
||
639 | 16 | if (empty($outputFileRelativePath)) { |
|
640 | 3 | $outputFileRelativePathParts = []; |
|
641 | } else { |
||
642 | 13 | $outputFileRelativePathParts = explode('/', $outputFileRelativePath); |
|
643 | } |
||
644 | |||
645 | 16 | $callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) { |
|
646 | 13 | $fullMatch = $matches[0]; |
|
647 | 13 | $inputUrl = $matches[1]; |
|
648 | |||
649 | 13 | if (strncmp($inputUrl, '/', 1) === 0 || strncmp($inputUrl, '#', 1) === 0 || preg_match('/^https?:\/\//i', $inputUrl) || preg_match('/^data:/i', $inputUrl)) { |
|
650 | 5 | return $fullMatch; |
|
651 | } |
||
652 | 8 | if ($inputFileRelativePathParts === $outputFileRelativePathParts) { |
|
653 | 1 | return $fullMatch; |
|
654 | } |
||
655 | |||
656 | 7 | if (empty($outputFileRelativePathParts)) { |
|
657 | 1 | $outputUrlParts = []; |
|
658 | } else { |
||
659 | 6 | $outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..'); |
|
660 | } |
||
661 | 7 | $outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts); |
|
662 | |||
663 | 7 | if (strpos($inputUrl, '/') !== false) { |
|
664 | 4 | $inputUrlParts = explode('/', $inputUrl); |
|
665 | 4 | foreach ($inputUrlParts as $key => $inputUrlPart) { |
|
666 | 4 | if ($inputUrlPart === '..') { |
|
667 | 4 | array_pop($outputUrlParts); |
|
668 | 4 | unset($inputUrlParts[$key]); |
|
669 | } |
||
670 | } |
||
671 | 4 | $outputUrlParts[] = implode('/', $inputUrlParts); |
|
672 | } else { |
||
673 | 3 | $outputUrlParts[] = $inputUrl; |
|
674 | } |
||
675 | 7 | $outputUrl = implode('/', $outputUrlParts); |
|
676 | |||
677 | 7 | return str_replace($inputUrl, $outputUrl, $fullMatch); |
|
678 | 16 | }; |
|
679 | |||
680 | 16 | $cssContent = preg_replace_callback('/url\(["\']?([^)^"\']*)["\']?\)/i', $callback, $cssContent); |
|
681 | |||
682 | 16 | return $cssContent; |
|
683 | } |
||
684 | |||
685 | /** |
||
686 | * Creates template of configuration file for [[actionCompress]]. |
||
687 | * @param string $configFile output file name. |
||
688 | * @return int CLI exit code |
||
689 | * @throws \yii\console\Exception on failure. |
||
690 | */ |
||
691 | 1 | public function actionTemplate($configFile) |
|
692 | { |
||
693 | 1 | $jsCompressor = VarDumper::export($this->jsCompressor); |
|
694 | 1 | $cssCompressor = VarDumper::export($this->cssCompressor); |
|
695 | |||
696 | 1 | $template = <<<EOD |
|
697 | 1 | <?php |
|
698 | /** |
||
699 | * Configuration file for the "yii asset" console command. |
||
700 | */ |
||
701 | |||
702 | // In the console environment, some path aliases may not exist. Please define these: |
||
703 | // Yii::setAlias('@webroot', __DIR__ . '/../web'); |
||
704 | // Yii::setAlias('@web', '/'); |
||
705 | |||
706 | return [ |
||
707 | // Adjust command/callback for JavaScript files compressing: |
||
708 | 1 | 'jsCompressor' => {$jsCompressor}, |
|
709 | // Adjust command/callback for CSS files compressing: |
||
710 | 1 | 'cssCompressor' => {$cssCompressor}, |
|
711 | // Whether to delete asset source after compression: |
||
712 | 'deleteSource' => false, |
||
713 | // The list of asset bundles to compress: |
||
714 | 'bundles' => [ |
||
715 | // 'app\assets\AppAsset', |
||
716 | // 'yii\web\YiiAsset', |
||
717 | // 'yii\web\JqueryAsset', |
||
718 | ], |
||
719 | // Asset bundle for compression output: |
||
720 | 'targets' => [ |
||
721 | 'all' => [ |
||
722 | 'class' => 'yii\web\AssetBundle', |
||
723 | 'basePath' => '@webroot/assets', |
||
724 | 'baseUrl' => '@web/assets', |
||
725 | 'js' => 'js/all-{hash}.js', |
||
726 | 'css' => 'css/all-{hash}.css', |
||
727 | ], |
||
728 | ], |
||
729 | // Asset manager configuration: |
||
730 | 'assetManager' => [ |
||
731 | //'basePath' => '@webroot/assets', |
||
732 | //'baseUrl' => '@web/assets', |
||
733 | ], |
||
734 | ]; |
||
735 | 1 | EOD; |
|
736 | 1 | if (file_exists($configFile)) { |
|
737 | if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) { |
||
738 | return ExitCode::OK; |
||
739 | } |
||
740 | } |
||
741 | 1 | if (!file_put_contents($configFile, $template, LOCK_EX)) { |
|
742 | throw new Exception("Unable to write template file '{$configFile}'."); |
||
743 | } |
||
744 | |||
745 | 1 | $this->stdout("Configuration file template created at '{$configFile}'.\n\n", Console::FG_GREEN); |
|
746 | 1 | return ExitCode::OK; |
|
747 | } |
||
748 | |||
749 | /** |
||
750 | * Returns canonicalized absolute pathname. |
||
751 | * Unlike regular `realpath()` this method does not expand symlinks and does not check path existence. |
||
752 | * @param string $path raw path |
||
753 | * @return string canonicalized absolute pathname |
||
754 | */ |
||
755 | 9 | private function findRealPath($path) |
|
756 | { |
||
757 | 9 | $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); |
|
758 | 9 | $pathParts = explode(DIRECTORY_SEPARATOR, $path); |
|
759 | |||
760 | 9 | $realPathParts = []; |
|
761 | 9 | foreach ($pathParts as $pathPart) { |
|
762 | 9 | if ($pathPart === '..') { |
|
763 | 4 | array_pop($realPathParts); |
|
764 | } else { |
||
765 | 9 | $realPathParts[] = $pathPart; |
|
766 | } |
||
767 | } |
||
768 | |||
769 | 9 | return implode(DIRECTORY_SEPARATOR, $realPathParts); |
|
770 | } |
||
771 | |||
772 | /** |
||
773 | * @param AssetBundle $bundle |
||
774 | * @return bool whether asset bundle external or not. |
||
775 | */ |
||
776 | 4 | private function isBundleExternal($bundle) |
|
777 | { |
||
778 | 4 | return empty($bundle->sourcePath) && empty($bundle->basePath); |
|
779 | } |
||
780 | |||
781 | /** |
||
782 | * @param AssetBundle $bundle asset bundle instance. |
||
783 | * @return array bundle configuration. |
||
784 | */ |
||
785 | 2 | private function composeBundleConfig($bundle) |
|
786 | { |
||
787 | 2 | $config = Yii::getObjectVars($bundle); |
|
788 | 2 | $config['class'] = get_class($bundle); |
|
789 | 2 | return $config; |
|
790 | } |
||
791 | |||
792 | /** |
||
793 | * Composes trace info for bundle circular dependency. |
||
794 | * @param string $circularDependencyName name of the bundle, which have circular dependency |
||
795 | * @param array $registered list of bundles registered while detecting circular dependency. |
||
796 | * @return string bundle circular dependency trace string. |
||
797 | */ |
||
798 | 1 | private function composeCircularDependencyTrace($circularDependencyName, array $registered) |
|
799 | { |
||
800 | 1 | $dependencyTrace = []; |
|
801 | 1 | $startFound = false; |
|
802 | 1 | foreach ($registered as $name => $value) { |
|
803 | 1 | if ($name === $circularDependencyName) { |
|
804 | 1 | $startFound = true; |
|
805 | } |
||
806 | 1 | if ($startFound && $value === false) { |
|
807 | 1 | $dependencyTrace[] = $name; |
|
808 | } |
||
809 | } |
||
810 | 1 | $dependencyTrace[] = $circularDependencyName; |
|
811 | 1 | return implode(' -> ', $dependencyTrace); |
|
812 | } |
||
813 | |||
814 | /** |
||
815 | * Deletes bundle asset files, which have been published from `sourcePath`. |
||
816 | * @param \yii\web\AssetBundle[] $bundles asset bundles to be processed. |
||
817 | * @since 2.0.10 |
||
818 | */ |
||
819 | 1 | private function deletePublishedAssets($bundles) |
|
820 | { |
||
821 | 1 | $this->stdout("Deleting source files...\n"); |
|
822 | |||
823 | 1 | if ($this->getAssetManager()->linkAssets) { |
|
824 | $this->stdout("`AssetManager::linkAssets` option is enabled. Deleting of source files canceled.\n", Console::FG_YELLOW); |
||
825 | return; |
||
826 | } |
||
827 | |||
828 | 1 | foreach ($bundles as $bundle) { |
|
829 | 1 | if ($bundle->sourcePath !== null) { |
|
830 | 1 | foreach ($bundle->js as $jsFile) { |
|
831 | 1 | @unlink($bundle->basePath . DIRECTORY_SEPARATOR . $jsFile); |
|
0 ignored issues
–
show
|
|||
832 | } |
||
833 | 1 | foreach ($bundle->css as $cssFile) { |
|
834 | 1 | @unlink($bundle->basePath . DIRECTORY_SEPARATOR . $cssFile); |
|
835 | } |
||
836 | } |
||
837 | } |
||
838 | |||
839 | 1 | $this->stdout("Source files deleted.\n", Console::FG_GREEN); |
|
840 | } |
||
841 | } |
||
842 |
If you suppress an error, we recommend checking for the error condition explicitly: