[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 final class CelerityResourceMapGenerator { 4 5 private $debug = false; 6 private $resources; 7 8 private $nameMap = array(); 9 private $symbolMap = array(); 10 private $requiresMap = array(); 11 private $packageMap = array(); 12 13 public function __construct(CelerityPhysicalResources $resources) { 14 $this->resources = $resources; 15 } 16 17 public function getNameMap() { 18 return $this->nameMap; 19 } 20 21 public function getSymbolMap() { 22 return $this->symbolMap; 23 } 24 25 public function getRequiresMap() { 26 return $this->requiresMap; 27 } 28 29 public function getPackageMap() { 30 return $this->packageMap; 31 } 32 33 public function setDebug($debug) { 34 $this->debug = $debug; 35 return $this; 36 } 37 38 protected function log($message) { 39 if ($this->debug) { 40 $console = PhutilConsole::getConsole(); 41 $console->writeErr("%s\n", $message); 42 } 43 } 44 45 public function generate() { 46 $binary_map = $this->rebuildBinaryResources($this->resources); 47 48 $this->log(pht('Found %d binary resources.', count($binary_map))); 49 50 $xformer = id(new CelerityResourceTransformer()) 51 ->setMinify(false) 52 ->setRawURIMap(ipull($binary_map, 'uri')); 53 54 $text_map = $this->rebuildTextResources($this->resources, $xformer); 55 56 $this->log(pht('Found %d text resources.', count($text_map))); 57 58 $resource_graph = array(); 59 $requires_map = array(); 60 $symbol_map = array(); 61 foreach ($text_map as $name => $info) { 62 if (isset($info['provides'])) { 63 $symbol_map[$info['provides']] = $info['hash']; 64 65 // We only need to check for cycles and add this to the requires map 66 // if it actually requires anything. 67 if (!empty($info['requires'])) { 68 $resource_graph[$info['provides']] = $info['requires']; 69 $requires_map[$info['hash']] = $info['requires']; 70 } 71 } 72 } 73 74 $this->detectGraphCycles($resource_graph); 75 $name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash'); 76 $hash_map = array_flip($name_map); 77 78 $package_map = $this->rebuildPackages( 79 $this->resources, 80 $symbol_map, 81 $hash_map); 82 83 $this->log(pht('Found %d packages.', count($package_map))); 84 85 $component_map = array(); 86 foreach ($package_map as $package_name => $package_info) { 87 foreach ($package_info['symbols'] as $symbol) { 88 $component_map[$symbol] = $package_name; 89 } 90 } 91 92 $name_map = $this->mergeNameMaps( 93 array( 94 array(pht('Binary'), ipull($binary_map, 'hash')), 95 array(pht('Text'), ipull($text_map, 'hash')), 96 array(pht('Package'), ipull($package_map, 'hash')), 97 )); 98 $package_map = ipull($package_map, 'symbols'); 99 100 ksort($name_map); 101 ksort($symbol_map); 102 ksort($requires_map); 103 ksort($package_map); 104 105 $this->nameMap = $name_map; 106 $this->symbolMap = $symbol_map; 107 $this->requiresMap = $requires_map; 108 $this->packageMap = $package_map; 109 110 return $this; 111 } 112 113 public function write() { 114 $map_content = $this->formatMapContent(array( 115 'names' => $this->getNameMap(), 116 'symbols' => $this->getSymbolMap(), 117 'requires' => $this->getRequiresMap(), 118 'packages' => $this->getPackageMap(), 119 )); 120 121 $map_path = $this->resources->getPathToMap(); 122 $this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path))); 123 Filesystem::writeFile($map_path, $map_content); 124 125 return $this; 126 } 127 128 private function formatMapContent(array $data) { 129 $content = phutil_var_export($data); 130 $generated = '@'.'generated'; 131 132 return <<<EOFILE 133 <?php 134 135 /** 136 * This file is automatically generated. Use 'bin/celerity map' to rebuild it. 137 * 138 * {$generated} 139 */ 140 return {$content}; 141 142 EOFILE; 143 } 144 145 /** 146 * Find binary resources (like PNG and SWF) and return information about 147 * them. 148 * 149 * @param CelerityPhysicalResources Resource map to find binary resources for. 150 * @return map<string, map<string, string>> Resource information map. 151 */ 152 private function rebuildBinaryResources( 153 CelerityPhysicalResources $resources) { 154 155 $binary_map = $resources->findBinaryResources(); 156 $result_map = array(); 157 158 foreach ($binary_map as $name => $data_hash) { 159 $hash = $resources->getCelerityHash($data_hash.$name); 160 161 $result_map[$name] = array( 162 'hash' => $hash, 163 'uri' => $resources->getResourceURI($hash, $name), 164 ); 165 } 166 167 return $result_map; 168 } 169 170 /** 171 * Find text resources (like JS and CSS) and return information about them. 172 * 173 * @param CelerityPhysicalResources Resource map to find text resources for. 174 * @param CelerityResourceTransformer Configured resource transformer. 175 * @return map<string, map<string, string>> Resource information map. 176 */ 177 private function rebuildTextResources( 178 CelerityPhysicalResources $resources, 179 CelerityResourceTransformer $xformer) { 180 181 $text_map = $resources->findTextResources(); 182 $result_map = array(); 183 184 foreach ($text_map as $name => $data_hash) { 185 $raw_data = $resources->getResourceData($name); 186 $xformed_data = $xformer->transformResource($name, $raw_data); 187 188 $data_hash = $resources->getCelerityHash($xformed_data); 189 $hash = $resources->getCelerityHash($data_hash.$name); 190 191 list($provides, $requires) = $this->getProvidesAndRequires( 192 $name, 193 $raw_data); 194 195 $result_map[$name] = array( 196 'hash' => $hash, 197 ); 198 199 if ($provides !== null) { 200 $result_map[$name] += array( 201 'provides' => $provides, 202 'requires' => $requires, 203 ); 204 } 205 } 206 207 return $result_map; 208 } 209 210 /** 211 * Parse the `@provides` and `@requires` symbols out of a text resource, like 212 * JS or CSS. 213 * 214 * @param string Resource name. 215 * @param string Resource data. 216 * @return pair<string|null, list<string>|null> The `@provides` symbol and 217 * the list of `@requires` symbols. If the resource is not part of the 218 * dependency graph, both are null. 219 */ 220 private function getProvidesAndRequires($name, $data) { 221 $parser = new PhutilDocblockParser(); 222 223 $matches = array(); 224 $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); 225 if (!$ok) { 226 throw new Exception( 227 pht( 228 'Resource "%s" does not have a header doc comment. Encode '. 229 'dependency data in a header docblock.', 230 $name)); 231 } 232 233 list($description, $metadata) = $parser->parse($matches[0]); 234 235 $provides = preg_split('/\s+/', trim(idx($metadata, 'provides'))); 236 $requires = preg_split('/\s+/', trim(idx($metadata, 'requires'))); 237 $provides = array_filter($provides); 238 $requires = array_filter($requires); 239 240 if (!$provides) { 241 // Tests and documentation-only JS is permitted to @provide no targets. 242 return array(null, null); 243 } 244 245 if (count($provides) > 1) { 246 throw new Exception( 247 pht('Resource "%s" must @provide at most one Celerity target.', $name)); 248 } 249 250 return array(head($provides), $requires); 251 } 252 253 /** 254 * Check for dependency cycles in the resource graph. Raises an exception if 255 * a cycle is detected. 256 * 257 * @param map<string, list<string>> Map of `@provides` symbols to their 258 * `@requires` symbols. 259 * @return void 260 */ 261 private function detectGraphCycles(array $nodes) { 262 $graph = id(new CelerityResourceGraph()) 263 ->addNodes($nodes) 264 ->setResourceGraph($nodes) 265 ->loadGraph(); 266 267 foreach ($nodes as $provides => $requires) { 268 $cycle = $graph->detectCycles($provides); 269 if ($cycle) { 270 throw new Exception( 271 pht('Cycle detected in resource graph: %s', implode(' > ', $cycle))); 272 } 273 } 274 } 275 276 /** 277 * Build package specifications for a given resource source. 278 * 279 * @param CelerityPhysicalResources Resource source to rebuild. 280 * @param map<string, string> Map of `@provides` to hashes. 281 * @param map<string, string> Map of hashes to resource names. 282 * @return map<string, map<string, string>> Package information maps. 283 */ 284 private function rebuildPackages( 285 CelerityPhysicalResources $resources, 286 array $symbol_map, 287 array $reverse_map) { 288 289 $package_map = array(); 290 291 $package_spec = $resources->getResourcePackages(); 292 foreach ($package_spec as $package_name => $package_symbols) { 293 $type = null; 294 $hashes = array(); 295 foreach ($package_symbols as $symbol) { 296 $symbol_hash = idx($symbol_map, $symbol); 297 if ($symbol_hash === null) { 298 throw new Exception( 299 pht( 300 'Package specification for "%s" includes "%s", but that symbol '. 301 'is not @provided by any resource.', 302 $package_name, 303 $symbol)); 304 } 305 306 $resource_name = $reverse_map[$symbol_hash]; 307 $resource_type = $resources->getResourceType($resource_name); 308 if ($type === null) { 309 $type = $resource_type; 310 } else if ($type !== $resource_type) { 311 throw new Exception( 312 pht( 313 'Package specification for "%s" includes resources of multiple '. 314 'types (%s, %s). Each package may only contain one type of '. 315 'resource.', 316 $package_name, 317 $type, 318 $resource_type)); 319 } 320 321 $hashes[] = $symbol.':'.$symbol_hash; 322 } 323 324 $hash = $resources->getCelerityHash(implode("\n", $hashes)); 325 $package_map[$package_name] = array( 326 'hash' => $hash, 327 'symbols' => $package_symbols, 328 ); 329 } 330 331 return $package_map; 332 } 333 334 private function mergeNameMaps(array $maps) { 335 $result = array(); 336 $origin = array(); 337 338 foreach ($maps as $map) { 339 list($map_name, $data) = $map; 340 foreach ($data as $name => $hash) { 341 if (empty($result[$name])) { 342 $result[$name] = $hash; 343 $origin[$name] = $map_name; 344 } else { 345 $old = $origin[$name]; 346 $new = $map_name; 347 throw new Exception( 348 pht( 349 'Resource source defines two resources with the same name, '. 350 '"%s". One is defined in the "%s" map; the other in the "%s" '. 351 'map. Each resource must have a unique name.', 352 $name, 353 $old, 354 $new)); 355 } 356 } 357 } 358 return $result; 359 } 360 361 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |