1<?php 2 3/* 4 * This file is part of Component Installer. 5 * 6 * (c) Rob Loach (http://robloach.net) 7 * 8 * For the full copyright and license information, please view the LICENSE.md 9 * file that was distributed with this source code. 10 */ 11 12namespace ComponentInstaller\Process; 13 14use Assetic\Asset\StringAsset; 15use Composer\Config; 16use Composer\Json\JsonFile; 17use Assetic\Asset\AssetCollection; 18use Assetic\Asset\FileAsset; 19 20/** 21 * Builds the require.js configuration. 22 */ 23class RequireJsProcess extends Process 24{ 25 /** 26 * The base URL for the require.js configuration. 27 */ 28 protected $baseUrl = 'components'; 29 30 /** 31 * {@inheritdoc} 32 */ 33 public function init() 34 { 35 $output = parent::init(); 36 if ($this->config->has('component-baseurl')) { 37 $this->baseUrl = $this->config->get('component-baseurl'); 38 } 39 40 return $output; 41 } 42 43 /** 44 * {@inheritdoc} 45 */ 46 public function process() 47 { 48 // Construct the require.js and stick it in the destination. 49 $json = $this->requireJson($this->packages, $this->config); 50 $requireConfig = $this->requireJs($json); 51 52 // Attempt to write the require.config.js file. 53 $destination = $this->componentDir . '/require.config.js'; 54 $this->fs->ensureDirectoryExists(dirname($destination)); 55 if (file_put_contents($destination, $requireConfig) === FALSE) { 56 $this->io->write('<error>Error writing require.config.js</error>'); 57 58 return false; 59 } 60 61 // Read in require.js to prepare the final require.js. 62 if (!file_exists(dirname(__DIR__) . '/Resources/require.js')) { 63 $this->io->write('<error>Error reading in require.js</error>'); 64 65 return false; 66 } 67 68 $assets = $this->newAssetCollection(); 69 $assets->add(new FileAsset(dirname(__DIR__) . '/Resources/require.js')); 70 $assets->add(new StringAsset($requireConfig)); 71 72 // Append the config to the require.js and write it. 73 if (file_put_contents($this->componentDir . '/require.js', $assets->dump()) === FALSE) { 74 $this->io->write('<error>Error writing require.js to the components directory</error>'); 75 76 return false; 77 } 78 79 return null; 80 } 81 82 /** 83 * Creates a require.js configuration from an array of packages. 84 * 85 * @param $packages 86 * An array of packages from the composer.lock file. 87 * 88 * @return array 89 * The built JSON array. 90 */ 91 public function requireJson(array $packages) 92 { 93 $json = array(); 94 95 // Construct the packages configuration. 96 foreach ($packages as $package) { 97 // Retrieve information from the extra options. 98 $extra = isset($package['extra']) ? $package['extra'] : array(); 99 $options = isset($extra['component']) ? $extra['component'] : array(); 100 101 // Construct the base details. 102 $name = $this->getComponentName($package['name'], $extra); 103 $component = array( 104 'name' => $name, 105 ); 106 107 // Build the "main" directive. 108 $scripts = isset($options['scripts']) ? $options['scripts'] : array(); 109 if (!empty($scripts)) { 110 // Put all scripts into a build.js file. 111 $result = $this->aggregateScripts($package, $scripts, $name.DIRECTORY_SEPARATOR.$name.'-built.js'); 112 if ($result) { 113 // If the aggregation was successful, add the script to the 114 // packages array. 115 $component['main'] = $name.'-built.js'; 116 117 // Add the component to the packages array. 118 $json['packages'][] = $component; 119 } 120 } 121 122 // Add the shim definition for the package. 123 $shim = isset($options['shim']) ? $options['shim'] : array(); 124 if (!empty($shim)) { 125 $json['shim'][$name] = $shim; 126 } 127 128 // Add the config definition for the package. 129 $packageConfig = isset($options['config']) ? $options['config'] : array(); 130 if (!empty($packageConfig)) { 131 $json['config'][$name] = $packageConfig; 132 } 133 } 134 135 // Provide the baseUrl. 136 $json['baseUrl'] = $this->baseUrl; 137 138 // Merge in configuration options from the root. 139 if ($this->config->has('component')) { 140 $config = $this->config->get('component'); 141 if (isset($config) && is_array($config)) { 142 // Use a recursive, distict array merge. 143 $json = $this->arrayMergeRecursiveDistinct($json, $config); 144 } 145 } 146 147 return $json; 148 } 149 150 /** 151 * Concatenate all scripts together into one destination file. 152 * 153 * @param array $package 154 * @param array $scripts 155 * @param string $file 156 * @return bool 157 */ 158 public function aggregateScripts($package, array $scripts, $file) 159 { 160 $assets = $this->newAssetCollection(); 161 162 foreach ($scripts as $script) { 163 // Collect each candidate from a glob file search. 164 $path = $this->getVendorDir($package).DIRECTORY_SEPARATOR.$script; 165 $matches = $this->fs->recursiveGlobFiles($path); 166 foreach ($matches as $match) { 167 $assets->add(new FileAsset($match)); 168 } 169 } 170 $js = $assets->dump(); 171 172 // Write the file if there are any JavaScript assets. 173 if (!empty($js)) { 174 $destination = $this->componentDir.DIRECTORY_SEPARATOR.$file; 175 $this->fs->ensureDirectoryExists(dirname($destination)); 176 177 return file_put_contents($destination, $js); 178 } 179 180 return false; 181 } 182 183 /** 184 * Constructs the require.js file from the provided require.js JSON array. 185 * 186 * @param $json 187 * The require.js JSON configuration. 188 * 189 * @return string 190 * The RequireJS JavaScript configuration. 191 */ 192 public function requireJs(array $json = array()) 193 { 194 // Encode the array to a JSON array. 195 $js = JsonFile::encode($json); 196 197 // Construct the JavaScript output. 198 $output = <<<EOT 199var components = $js; 200if (typeof require !== "undefined" && require.config) { 201 require.config(components); 202} else { 203 var require = components; 204} 205if (typeof exports !== "undefined" && typeof module !== "undefined") { 206 module.exports = components; 207} 208EOT; 209 210 return $output; 211 } 212 213 /** 214 * Merges two arrays without changing string array keys. Appends to array if keys are numeric. 215 * 216 * @see array_merge() 217 * @see array_merge_recursive() 218 * 219 * @param array $array1 220 * @param array $array2 221 * @return array 222 */ 223 protected function arrayMergeRecursiveDistinct(array &$array1, array &$array2) 224 { 225 $merged = $array1; 226 227 foreach ($array2 as $key => &$value) { 228 if(is_numeric($key)){ 229 $merged[] = $value; 230 } else { 231 if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { 232 $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value); 233 } 234 else { 235 $merged[$key] = $value; 236 } 237 } 238 } 239 240 return $merged; 241 } 242 243 /** 244 * @return AssetCollection 245 */ 246 protected function newAssetCollection() 247 { 248 // Aggregate all the assets into one file. 249 $assets = new AssetCollection(); 250 if ($this->config->has('component-scriptFilters')) { 251 $filters = $this->config->get('component-scriptFilters'); 252 if (isset($filters) && is_array($filters)) { 253 foreach ($filters as $filter => $filterParams) { 254 $reflection = new \ReflectionClass($filter); 255 /** @var \Assetic\Filter\FilterInterface $filter */ 256 $filter = $reflection->newInstanceArgs($filterParams); 257 $assets->ensureFilter($filter); 258 } 259 } 260 } 261 262 return $assets; 263 } 264} 265