diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -107,6 +107,7 @@ 'CelerityResourceController' => 'infrastructure/celerity/CelerityResourceController.php', 'CelerityResourceGraph' => 'infrastructure/celerity/CelerityResourceGraph.php', 'CelerityResourceMap' => 'infrastructure/celerity/CelerityResourceMap.php', + 'CelerityResourceMapGenerator' => 'infrastructure/celerity/CelerityResourceMapGenerator.php', 'CelerityResourceTransformer' => 'infrastructure/celerity/CelerityResourceTransformer.php', 'CelerityResourceTransformerTestCase' => 'infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php', 'CelerityResources' => 'infrastructure/celerity/resources/CelerityResources.php', @@ -1357,6 +1358,7 @@ 'PhabricatorCalendarPHIDTypeEvent' => 'applications/calendar/phid/PhabricatorCalendarPHIDTypeEvent.php', 'PhabricatorCalendarViewController' => 'applications/calendar/controller/PhabricatorCalendarViewController.php', 'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php', + 'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php', 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php', @@ -4168,6 +4170,7 @@ 'PhabricatorCalendarPHIDTypeEvent' => 'PhabricatorPHIDType', 'PhabricatorCalendarViewController' => 'PhabricatorCalendarController', 'PhabricatorCampfireProtocolAdapter' => 'PhabricatorBotBaseStreamingProtocolAdapter', + 'PhabricatorCelerityTestCase' => 'PhabricatorTestCase', 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChatLogChannel' => diff --git a/src/__tests__/PhabricatorCelerityTestCase.php b/src/__tests__/PhabricatorCelerityTestCase.php new file mode 100644 --- /dev/null +++ b/src/__tests__/PhabricatorCelerityTestCase.php @@ -0,0 +1,33 @@ +<?php + +final class PhabricatorCelerityTestCase extends PhabricatorTestCase { + + /** + * This is more of an acceptance test case instead of a unit test. It verifies + * that the Celerity map is up-to-date. + */ + public function testCelerityMaps() { + $resources_map = CelerityPhysicalResources::getAll(); + + foreach ($resources_map as $resources) { + $old_map = new CelerityResourceMap($resources); + + $new_map = id(new CelerityResourceMapGenerator($resources)) + ->generate(); + + $this->assertEqual( + $new_map->getNameMap(), + $old_map->getNameMap()); + $this->assertEqual( + $new_map->getSymbolMap(), + $old_map->getSymbolMap()); + $this->assertEqual( + $new_map->getRequiresMap(), + $old_map->getRequiresMap()); + $this->assertEqual( + $new_map->getPackageMap(), + $old_map->getPackageMap()); + } + } + +} diff --git a/src/infrastructure/celerity/CelerityResourceMap.php b/src/infrastructure/celerity/CelerityResourceMap.php --- a/src/infrastructure/celerity/CelerityResourceMap.php +++ b/src/infrastructure/celerity/CelerityResourceMap.php @@ -53,6 +53,22 @@ return self::$instances[$name]; } + 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 getPackagedNamesForSymbols(array $symbols) { $resolved = $this->resolveResources($symbols); return $this->packageResources($resolved); diff --git a/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php b/src/infrastructure/celerity/CelerityResourceMapGenerator.php copy from src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php copy to src/infrastructure/celerity/CelerityResourceMapGenerator.php --- a/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php +++ b/src/infrastructure/celerity/CelerityResourceMapGenerator.php @@ -1,64 +1,59 @@ <?php -final class CelerityManagementMapWorkflow - extends CelerityManagementWorkflow { - - public function didConstruct() { - $this - ->setName('map') - ->setExamples('**map** [options]') - ->setSynopsis(pht('Rebuild static resource maps.')) - ->setArguments( - array()); +final class CelerityResourceMapGenerator { + + 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 execute(PhutilArgumentParser $args) { - $resources_map = CelerityPhysicalResources::getAll(); + public function getNameMap() { + return $this->nameMap; + } - $this->log( - pht( - 'Rebuilding %d resource source(s).', - new PhutilNumber(count($resources_map)))); + public function getSymbolMap() { + return $this->symbolMap; + } - foreach ($resources_map as $name => $resources) { - $this->rebuildResources($resources); - } + public function getRequiresMap() { + return $this->requiresMap; + } - $this->log(pht('Done.')); + public function getPackageMap() { + return $this->packageMap; + } - return 0; + public function setDebug($debug) { + $this->debug = $debug; + return $this; } - /** - * Rebuild the resource map for a resource source. - * - * @param CelerityPhysicalResources Resource source to rebuild. - * @return void - */ - private function rebuildResources(CelerityPhysicalResources $resources) { - $this->log( - pht( - 'Rebuilding resource source "%s" (%s)...', - $resources->getName(), - get_class($resources))); + protected function log($message) { + if ($this->debug) { + $console = PhutilConsole::getConsole(); + $console->writeErr("%s\n", $message); + } + } - $binary_map = $this->rebuildBinaryResources($resources); + public function generate() { + $binary_map = $this->rebuildBinaryResources($this->resources); - $this->log( - pht( - 'Found %d binary resources.', - new PhutilNumber(count($binary_map)))); + $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($resources, $xformer); + $text_map = $this->rebuildTextResources($this->resources, $xformer); - $this->log( - pht( - 'Found %d text resources.', - new PhutilNumber(count($text_map)))); + $this->log(pht('Found %d text resources.', count($text_map))); $resource_graph = array(); $requires_map = array(); @@ -81,14 +76,11 @@ $hash_map = array_flip($name_map); $package_map = $this->rebuildPackages( - $resources, + $this->resources, $symbol_map, $hash_map); - $this->log( - pht( - 'Found %d packages.', - new PhutilNumber(count($package_map)))); + $this->log(pht('Found %d packages.', count($package_map))); $component_map = array(); foreach ($package_map as $package_name => $package_info) { @@ -110,18 +102,46 @@ ksort($requires_map); ksort($package_map); + $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' => $name_map, - 'symbols' => $symbol_map, - 'requires' => $requires_map, - 'packages' => $package_map, + 'names' => $this->getNameMap(), + 'symbols' => $this->getSymbolMap(), + 'requires' => $this->getRequiresMap(), + 'packages' => $this->getPackageMap(), )); - $map_path = $resources->getPathToMap(); + $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 = var_export($data, true); + $content = preg_replace('/\s+$/m', '', $content); + $content = preg_replace('/array \(/', 'array(', $content); + + $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 @@ -134,21 +154,20 @@ CelerityPhysicalResources $resources) { $binary_map = $resources->findBinaryResources(); - $result_map = array(); + foreach ($binary_map as $name => $data_hash) { $hash = $resources->getCelerityHash($data_hash.$name); $result_map[$name] = array( 'hash' => $hash, - 'uri' => $resources->getResourceURI($hash, $name), + 'uri' => $resources->getResourceURI($hash, $name), ); } return $result_map; } - /** * Find text resources (like JS and CSS) and return information about them. * @@ -161,8 +180,8 @@ 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); @@ -189,15 +208,14 @@ return $result_map; } - /** * Parse the `@provides` and `@requires` symbols out of a text resource, like * JS or CSS. * * @param string Resource name. * @param string Resource data. - * @return pair<string|null, list<string>|null> The `@provides` symbol and the - * list of `@requires` symbols. If the resource is not part of the + * @return pair<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) { @@ -227,15 +245,12 @@ if (count($provides) > 1) { throw new Exception( - pht( - 'Resource "%s" must @provide at most one Celerity target.', - $name)); + pht('Resource "%s" must @provide at most one Celerity target.', $name)); } return array(head($provides), $requires); } - /** * Check for dependency cycles in the resource graph. Raises an exception if * a cycle is detected. @@ -254,9 +269,7 @@ $cycle = $graph->detectCycles($provides); if ($cycle) { throw new Exception( - pht( - 'Cycle detected in resource graph: %s', - implode(' > ', $cycle))); + pht('Cycle detected in resource graph: %s', implode(' > ', $cycle))); } } } @@ -265,8 +278,8 @@ * Build package specifications for a given resource source. * * @param CelerityPhysicalResources Resource source to rebuild. - * @param list<string, string> Map of `@provides` to hashes. - * @param list<string, string> Map of hashes to resource names. + * @param map<string, string> Map of `@provides` to hashes. + * @param map<string, string> Map of hashes to resource names. * @return map<string, map<string, string>> Package information maps. */ private function rebuildPackages( @@ -322,6 +335,7 @@ private function mergeNameMaps(array $maps) { $result = array(); $origin = array(); + foreach ($maps as $map) { list($map_name, $data) = $map; foreach ($data as $name => $hash) { @@ -345,28 +359,4 @@ return $result; } - private function log($message) { - $console = PhutilConsole::getConsole(); - $console->writeErr("%s\n", $message); - } - - private function formatMapContent(array $data) { - $content = var_export($data, true); - $content = preg_replace('/\s+$/m', '', $content); - $content = preg_replace('/array \(/', 'array(', $content); - - $generated = '@'.'generated'; - return <<<EOFILE -<?php - -/** - * This file is automatically generated. Use 'bin/celerity map' to rebuild it. - * {$generated} - */ -return {$content}; - -EOFILE; - } - - } diff --git a/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php b/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php --- a/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php +++ b/src/infrastructure/celerity/management/CelerityManagementMapWorkflow.php @@ -42,331 +42,15 @@ $resources->getName(), get_class($resources))); - $binary_map = $this->rebuildBinaryResources($resources); - - $this->log( - pht( - 'Found %d binary resources.', - new PhutilNumber(count($binary_map)))); - - $xformer = id(new CelerityResourceTransformer()) - ->setMinify(false) - ->setRawURIMap(ipull($binary_map, 'uri')); - - $text_map = $this->rebuildTextResources($resources, $xformer); - - $this->log( - pht( - 'Found %d text resources.', - new PhutilNumber(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( - $resources, - $symbol_map, - $hash_map); - - $this->log( - pht( - 'Found %d packages.', - new PhutilNumber(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); - ksort($symbol_map); - ksort($requires_map); - ksort($package_map); - - $map_content = $this->formatMapContent(array( - 'names' => $name_map, - 'symbols' => $symbol_map, - 'requires' => $requires_map, - 'packages' => $package_map, - )); - - $map_path = $resources->getPathToMap(); - $this->log(pht('Writing map "%s".', Filesystem::readablePath($map_path))); - Filesystem::writeFile($map_path, $map_content); - } - - - /** - * Find binary resources (like PNG and SWF) and return information about - * them. - * - * @param CelerityPhysicalResources 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 = $resources->getCelerityHash($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 Resource map to find text resources for. - * @param CelerityResourceTransformer 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 = $resources->getCelerityHash($xformed_data); - $hash = $resources->getCelerityHash($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; + id(new CelerityResourceMapGenerator($resources)) + ->setDebug(true) + ->generate() + ->write(); } - - /** - * Parse the `@provides` and `@requires` symbols out of a text resource, like - * JS or CSS. - * - * @param string Resource name. - * @param string Resource data. - * @return pair<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 = preg_split('/\s+/', trim(idx($metadata, 'provides'))); - $requires = preg_split('/\s+/', trim(idx($metadata, 'requires'))); - $provides = array_filter($provides); - $requires = array_filter($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 @provide at most one Celerity target.', - $name)); - } - - 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>> 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 Resource source to rebuild. - * @param list<string, string> Map of `@provides` to hashes. - * @param list<string, string> 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 @provided by any resource.', - $package_name, - $symbol)); - } - - $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 = $resources->getCelerityHash(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 log($message) { + protected function log($message) { $console = PhutilConsole::getConsole(); $console->writeErr("%s\n", $message); } - private function formatMapContent(array $data) { - $content = var_export($data, true); - $content = preg_replace('/\s+$/m', '', $content); - $content = preg_replace('/array \(/', 'array(', $content); - - $generated = '@'.'generated'; - return <<<EOFILE -<?php - -/** - * This file is automatically generated. Use 'bin/celerity map' to rebuild it. - * {$generated} - */ -return {$content}; - -EOFILE; - } - - }