Something went wrong. Try again.
@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
Something went wrong. Try again.
12 kB · 410 lines
PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411<?php
final class CelerityResourceMapGenerator extends Phobject {
private $debug = false; private $resources;
private $nameMap = array(); private $symbolMap = array(); private $requiresMap = array(); private $packageMap = array();
public function __construct(CelerityPhysicalResources $resources) { $this->resources = $resources; }
public function getNameMap() { return $this->nameMap; }
public function getSymbolMap() { return $this->symbolMap; }
public function getRequiresMap() { return $this->requiresMap; }
public function getPackageMap() { return $this->packageMap; }
public function setDebug($debug) { $this->debug = $debug; return $this; }
protected function log($message) { if ($this->debug) { $console = PhutilConsole::getConsole(); $console->writeErr("%s\n", $message); } }
public function generate() { $binary_map = $this->rebuildBinaryResources($this->resources);
$this->log(pht('Found %d binary resources.', count($binary_map)));
$xformer = id(new CelerityResourceTransformer()) ->setMinify(false) ->setRawURIMap(ipull($binary_map, 'uri'));
$text_map = $this->rebuildTextResources($this->resources, $xformer);
$this->log(pht('Found %d text resources.', count($text_map)));
$resource_graph = array(); $requires_map = array(); $symbol_map = array(); foreach ($text_map as $name => $info) { if (isset($info['provides'])) { $symbol_map[$info['provides']] = $info['hash'];
// We only need to check for cycles and add this to the requires map // if it actually requires anything. if (!empty($info['requires'])) { $resource_graph[$info['provides']] = $info['requires']; $requires_map[$info['hash']] = $info['requires']; } } }
$this->detectGraphCycles($resource_graph); $name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash'); $hash_map = array_flip($name_map);
$package_map = $this->rebuildPackages( $this->resources, $symbol_map, $hash_map);
$this->log(pht('Found %d packages.', count($package_map)));
$component_map = array(); foreach ($package_map as $package_name => $package_info) { foreach ($package_info['symbols'] as $symbol) { $component_map[$symbol] = $package_name; } }
$name_map = $this->mergeNameMaps( array( array(pht('Binary'), ipull($binary_map, 'hash')), array(pht('Text'), ipull($text_map, 'hash')), array(pht('Package'), ipull($package_map, 'hash')), )); $package_map = ipull($package_map, 'symbols');
ksort($name_map, SORT_STRING); ksort($symbol_map, SORT_STRING); ksort($requires_map, SORT_STRING); ksort($package_map, SORT_STRING);
$this->nameMap = $name_map; $this->symbolMap = $symbol_map; $this->requiresMap = $requires_map; $this->packageMap = $package_map;
return $this; }
public function write() { $map_content = $this->formatMapContent(array( 'names' => $this->getNameMap(), 'symbols' => $this->getSymbolMap(), 'requires' => $this->getRequiresMap(), 'packages' => $this->getPackageMap(), ));
$map_path = $this->resources->getPathToMap(); $this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path))); Filesystem::writeFile($map_path, $map_content);
return $this; }
private function formatMapContent(array $data) { $content = phutil_var_export($data); $generated = '@'.'generated';
return <<<EOFILE<?php
/** * This file is automatically generated. Use 'bin/celerity map' to rebuild it. * * {$generated} */return {$content};
EOFILE; }
/** * Find binary resources (like PNG and SWF) and return information about * them. * * @param CelerityPhysicalResources $resources Resource map to find binary * resources for. * @return map<string, map<string, string>> Resource information map. */ private function rebuildBinaryResources( CelerityPhysicalResources $resources) {
$binary_map = $resources->findBinaryResources(); $result_map = array();
foreach ($binary_map as $name => $data_hash) { $hash = $this->newResourceHash($data_hash.$name);
$result_map[$name] = array( 'hash' => $hash, 'uri' => $resources->getResourceURI($hash, $name), ); }
return $result_map; }
/** * Find text resources (like JS and CSS) and return information about them. * * @param CelerityPhysicalResources $resources Resource map to find text * resources for. * @param CelerityResourceTransformer $xformer Configured resource * transformer. * @return map<string, map<string, string>> Resource information map. */ private function rebuildTextResources( CelerityPhysicalResources $resources, CelerityResourceTransformer $xformer) {
$text_map = $resources->findTextResources(); $result_map = array();
foreach ($text_map as $name => $data_hash) { $raw_data = $resources->getResourceData($name); $xformed_data = $xformer->transformResource($name, $raw_data);
$data_hash = $this->newResourceHash($xformed_data); $hash = $this->newResourceHash($data_hash.$name);
list($provides, $requires) = $this->getProvidesAndRequires( $name, $raw_data);
$result_map[$name] = array( 'hash' => $hash, );
if ($provides !== null) { $result_map[$name] += array( 'provides' => $provides, 'requires' => $requires, ); } }
return $result_map; }
/** * Parse the `@provides` and `@requires` symbols out of a text resource, like * JS or CSS. * * @param string $name Resource name. * @param string $data Resource data. * @return array<string|null, list<string>|null> The `@provides` symbol and * the list of `@requires` symbols. If the resource is not part of the * dependency graph, both are null. */ private function getProvidesAndRequires($name, $data) { $parser = new PhutilDocblockParser();
$matches = array(); $ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches); if (!$ok) { throw new Exception( pht( 'Resource "%s" does not have a header doc comment. Encode '. 'dependency data in a header docblock.', $name)); }
list($description, $metadata) = $parser->parse($matches[0]);
$provides = $this->parseResourceSymbolList(idx($metadata, 'provides')); $requires = $this->parseResourceSymbolList(idx($metadata, 'requires')); if (!$provides) { // Tests and documentation-only JS is permitted to @provide no targets. return array(null, null); }
if (count($provides) > 1) { throw new Exception( pht( 'Resource "%s" must %s at most one Celerity target.', $name, '@provide')); }
return array(head($provides), $requires); }
/** * Check for dependency cycles in the resource graph. Raises an exception if * a cycle is detected. * * @param map<string, list<string>> $nodes Map of `@provides` symbols to * their `@requires` symbols. * @return void */ private function detectGraphCycles(array $nodes) { $graph = id(new CelerityResourceGraph()) ->addNodes($nodes) ->setResourceGraph($nodes) ->loadGraph();
foreach ($nodes as $provides => $requires) { $cycle = $graph->detectCycles($provides); if ($cycle) { throw new Exception( pht( 'Cycle detected in resource graph: %s', implode(' > ', $cycle))); } } }
/** * Build package specifications for a given resource source. * * @param CelerityPhysicalResources $resources Resource source to rebuild. * @param map<string, string> $symbol_map Map of `@provides` to hashes. * @param map<string, string> $reverse_map Map of hashes to resource names. * @return map<string, map<string, string>> Package information maps. */ private function rebuildPackages( CelerityPhysicalResources $resources, array $symbol_map, array $reverse_map) {
$package_map = array();
$package_spec = $resources->getResourcePackages(); foreach ($package_spec as $package_name => $package_symbols) { $type = null; $hashes = array(); foreach ($package_symbols as $symbol) { $symbol_hash = idx($symbol_map, $symbol); if ($symbol_hash === null) { throw new Exception( pht( 'Package specification for "%s" includes "%s", but that symbol '. 'is not %s by any resource.', $package_name, $symbol, '@provided')); }
$resource_name = $reverse_map[$symbol_hash]; $resource_type = $resources->getResourceType($resource_name); if ($type === null) { $type = $resource_type; } else if ($type !== $resource_type) { throw new Exception( pht( 'Package specification for "%s" includes resources of multiple '. 'types (%s, %s). Each package may only contain one type of '. 'resource.', $package_name, $type, $resource_type)); }
$hashes[] = $symbol.':'.$symbol_hash; }
$hash = $this->newResourceHash(implode("\n", $hashes)); $package_map[$package_name] = array( 'hash' => $hash, 'symbols' => $package_symbols, ); }
return $package_map; }
private function mergeNameMaps(array $maps) { $result = array(); $origin = array();
foreach ($maps as $map) { list($map_name, $data) = $map; foreach ($data as $name => $hash) { if (empty($result[$name])) { $result[$name] = $hash; $origin[$name] = $map_name; } else { $old = $origin[$name]; $new = $map_name; throw new Exception( pht( 'Resource source defines two resources with the same name, '. '"%s". One is defined in the "%s" map; the other in the "%s" '. 'map. Each resource must have a unique name.', $name, $old, $new)); } } } return $result; }
private function parseResourceSymbolList($list) { if (!$list) { return array(); }
// This is valid: // // @requires x y // // But so is this: // // @requires x // @requires y // // Accept either form and produce a list of symbols.
$list = (array)$list;
// We can get `true` values if there was a bare `@requires` in the input. foreach ($list as $key => $item) { if ($item === true) { unset($list[$key]); } }
$list = implode(' ', $list); $list = trim($list); $list = preg_split('/\s+/', $list); $list = array_filter($list);
return $list; }
private function newResourceHash($data) { // This HMAC key is a static, hard-coded value because we don't want the // hashes in the map to depend on database state: when two different // developers regenerate the map, they should end up with the same output.
$hash = PhabricatorHash::digestHMACSHA256($data, 'celerity-resource-data');
return substr($hash, 0, 8); }
}