1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 06:42:42 +01:00

Drive all Celerity operations from the new map

Summary:
Ref T4222.

  - Removes the old map and changes the CelerityResourceMap API to be entirely driven by the new map.
  - The new map is about 50% smaller and organized more sensibly.
  - This removes the `/pkg/` URI component. All resources are now required to have unique names, so we can tell if a resource is a package or not by looking at the name.
  - Removes some junky old APIs.
  - Cleans up some other APIs.
  - Added some feedback for `bin/celerity map`.
  - `CelerityResourceMap` is still a singleton which is inextricably bound to the Phabricator map; this will change in the future.

Test Plan:
  - Reloaded pages.
  - Verified packaging works by looking at generated includes.
  - Forced minification on and verified it worked.
  - Forced no-timestamps on and verified it worked.
  - Rebuilt map.
  - Ran old script and verified error message.
  - Checked logs.

Reviewers: btrahan, hach-que

Reviewed By: hach-que

CC: chad, aran

Maniphest Tasks: T4222

Differential Revision: https://secure.phabricator.com/D7872
This commit is contained in:
epriestley 2013-12-31 18:04:25 -08:00
parent 60cb65bfbb
commit 2c35532256
19 changed files with 2665 additions and 5334 deletions

1
.gitignore vendored
View file

@ -1,6 +1,5 @@
.DS_Store .DS_Store
._* ._*
/webroot/rsrc/custom
.#* .#*
*# *#
*~ *~

2173
resources/celerity/map.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,180 @@
<?php
return array(
'javelin.pkg.js' => array(
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
),
'core.pkg.js' => array(
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phabricator-menu-item',
'phabricator-dropdown-menu',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-konami',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phabricator-hovercard',
'javelin-behavior-phabricator-hovercards',
'javelin-color',
'javelin-fx',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'phabricator-jump-nav',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'aphront-pager-view-css',
'phabricator-transaction-view-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'aphront-error-view-css',
'sprite-icons-css',
'sprite-gradient-css',
'sprite-menu-css',
'sprite-apps-large-css',
'sprite-status-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'lightbox-attachment-css',
'phui-header-view-css',
'phabricator-filetree-view-css',
'phabricator-nav-view-css',
'phabricator-side-menu-view-css',
'phabricator-crumbs-view-css',
'phui-object-item-list-view-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-application-launch-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phabricator-tag-view-css',
'phui-list-view-css',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-results-table-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'differential-revision-comment-list-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'differential-local-commits-view-css',
'inline-comment-summary-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-feedback-preview',
'javelin-behavior-differential-edit-inline-comments',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-show-more',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-differential-accept-with-errors',
'javelin-behavior-differential-comment-jump',
'javelin-behavior-differential-add-reviewers-and-ccs',
'javelin-behavior-differential-keyboard-navigation',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-load-blame',
'differential-inline-comment-editor',
'javelin-behavior-differential-dropdown-menus',
'javelin-behavior-differential-toggle-files',
'javelin-behavior-differential-user-select',
),
'diffusion.pkg.css' => array(
'diffusion-commit-view-css',
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
'phabricator-project-tag-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-transaction-controls',
'javelin-behavior-maniphest-transaction-preview',
'javelin-behavior-maniphest-transaction-expand',
'javelin-behavior-maniphest-subpriority-editor',
),
'darkconsole.pkg.js' => array(
'javelin-behavior-dark-console',
'javelin-behavior-error-log',
),
);

View file

@ -1,9 +1,9 @@
#!/bin/sh #!/bin/sh
echo "src/__celerity_resource_map__.php merge=celerity" \ echo "resources/celerity/map.php merge=celerity" \
>> `dirname "$0"`/../../.git/info/attributes >> `dirname "$0"`/../../.git/info/attributes
git config merge.celerity.name "Celerity Mapper" git config merge.celerity.name "Celerity Mapper"
git config merge.celerity.driver \ git config merge.celerity.driver \
'php $GIT_DIR/../scripts/celerity_mapper.php $GIT_DIR/../webroot' 'php $GIT_DIR/../bin/celerity map'

View file

@ -1,397 +1,5 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
$package_spec = array( echo "This script is obsolete. Run `bin/celerity map` instead.\n";
'javelin.pkg.js' => array( exit(1);
'javelin-util',
'javelin-install',
'javelin-event',
'javelin-stratcom',
'javelin-behavior',
'javelin-resource',
'javelin-request',
'javelin-vector',
'javelin-dom',
'javelin-json',
'javelin-uri',
'javelin-workflow',
'javelin-mask',
'javelin-typeahead',
'javelin-typeahead-normalizer',
'javelin-typeahead-source',
'javelin-typeahead-preloaded-source',
'javelin-typeahead-ondemand-source',
'javelin-tokenizer',
'javelin-history',
),
'core.pkg.js' => array(
'javelin-behavior-aphront-basic-tokenizer',
'javelin-behavior-workflow',
'javelin-behavior-aphront-form-disable-on-submit',
'phabricator-keyboard-shortcut-manager',
'phabricator-keyboard-shortcut',
'javelin-behavior-phabricator-keyboard-shortcuts',
'javelin-behavior-refresh-csrf',
'javelin-behavior-phabricator-watch-anchor',
'javelin-behavior-phabricator-autofocus',
'phabricator-menu-item',
'phabricator-dropdown-menu',
'phabricator-phtize',
'javelin-behavior-phabricator-oncopy',
'phabricator-tooltip',
'javelin-behavior-phabricator-tooltips',
'phabricator-prefab',
'javelin-behavior-device',
'javelin-behavior-toggle-class',
'javelin-behavior-lightbox-attachments',
'phabricator-busy',
'javelin-aphlict',
'phabricator-notification',
'javelin-behavior-aphlict-listen',
'javelin-behavior-phabricator-search-typeahead',
'javelin-behavior-konami',
'javelin-behavior-aphlict-dropdown',
'javelin-behavior-history-install',
'javelin-behavior-phabricator-gesture',
'javelin-behavior-phabricator-active-nav',
'javelin-behavior-phabricator-nav',
'javelin-behavior-phabricator-remarkup-assist',
'phabricator-textareautils',
'phabricator-file-upload',
'javelin-behavior-global-drag-and-drop',
'javelin-behavior-phabricator-reveal-content',
'phabricator-hovercard',
'javelin-behavior-phabricator-hovercards',
'javelin-color',
'javelin-fx',
),
'core.pkg.css' => array(
'phabricator-core-css',
'phabricator-zindex-css',
'phui-button-css',
'phabricator-standard-page-view',
'aphront-dialog-view-css',
'phui-form-view-css',
'aphront-panel-view-css',
'aphront-table-view-css',
'aphront-tokenizer-control-css',
'aphront-typeahead-control-css',
'aphront-list-filter-view-css',
'phabricator-jump-nav',
'phabricator-remarkup-css',
'syntax-highlighting-css',
'aphront-pager-view-css',
'phabricator-transaction-view-css',
'aphront-tooltip-css',
'phabricator-flag-css',
'aphront-error-view-css',
'sprite-icons-css',
'sprite-gradient-css',
'sprite-menu-css',
'sprite-apps-large-css',
'sprite-status-css',
'phabricator-main-menu-view',
'phabricator-notification-css',
'phabricator-notification-menu-css',
'lightbox-attachment-css',
'phui-header-view-css',
'phabricator-filetree-view-css',
'phabricator-nav-view-css',
'phabricator-side-menu-view-css',
'phabricator-crumbs-view-css',
'phui-object-item-list-view-css',
'global-drag-and-drop-css',
'phui-spacing-css',
'phui-form-css',
'phui-icon-view-css',
'phabricator-application-launch-view-css',
'phabricator-action-list-view-css',
'phui-property-list-view-css',
'phabricator-tag-view-css',
'phui-list-view-css',
),
'differential.pkg.css' => array(
'differential-core-view-css',
'differential-changeset-view-css',
'differential-results-table-css',
'differential-revision-history-css',
'differential-revision-list-css',
'differential-table-of-contents-css',
'differential-revision-comment-css',
'differential-revision-add-comment-css',
'differential-revision-comment-list-css',
'phabricator-object-selector-css',
'phabricator-content-source-view-css',
'differential-local-commits-view-css',
'inline-comment-summary-css',
),
'differential.pkg.js' => array(
'phabricator-drag-and-drop-file-upload',
'phabricator-shaped-request',
'javelin-behavior-differential-feedback-preview',
'javelin-behavior-differential-edit-inline-comments',
'javelin-behavior-differential-populate',
'javelin-behavior-differential-show-more',
'javelin-behavior-differential-diff-radios',
'javelin-behavior-differential-accept-with-errors',
'javelin-behavior-differential-comment-jump',
'javelin-behavior-differential-add-reviewers-and-ccs',
'javelin-behavior-differential-keyboard-navigation',
'javelin-behavior-aphront-drag-and-drop-textarea',
'javelin-behavior-phabricator-object-selector',
'javelin-behavior-repository-crossreference',
'javelin-behavior-load-blame',
'differential-inline-comment-editor',
'javelin-behavior-differential-dropdown-menus',
'javelin-behavior-differential-toggle-files',
'javelin-behavior-differential-user-select',
),
'diffusion.pkg.css' => array(
'diffusion-commit-view-css',
'diffusion-icons-css',
),
'diffusion.pkg.js' => array(
'javelin-behavior-diffusion-pull-lastmodified',
'javelin-behavior-diffusion-commit-graph',
'javelin-behavior-audit-preview',
),
'maniphest.pkg.css' => array(
'maniphest-task-summary-css',
'phabricator-project-tag-css',
),
'maniphest.pkg.js' => array(
'javelin-behavior-maniphest-batch-selector',
'javelin-behavior-maniphest-transaction-controls',
'javelin-behavior-maniphest-transaction-preview',
'javelin-behavior-maniphest-transaction-expand',
'javelin-behavior-maniphest-subpriority-editor',
),
'darkconsole.pkg.js' => array(
'javelin-behavior-dark-console',
'javelin-behavior-error-log',
),
);
require_once dirname(__FILE__).'/__init_script__.php';
$args = new PhutilArgumentParser($argv);
$args->setTagline('map static resources');
$args->setSynopsis(
"**celerity_mapper.php** [--output __path__] [--with-custom] <webroot>");
$args->parse(
array(
array(
'name' => 'webroot',
'wildcard' => true,
),
));
$root = $args->getArg('webroot');
if (count($root) != 1 || !is_dir(reset($root))) {
$args->printHelpAndExit();
}
$root = Filesystem::resolvePath(reset($root));
$celerity_path = Filesystem::resolvePath(
'../src/__celerity_resource_map__.php',
$root);
$resource_hash = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
$runtime_map = array();
echo "Finding raw static resources...\n";
$finder = id(new FileFinder($root))
->withType('f')
->withSuffix('png')
->withSuffix('jpg')
->withSuffix('gif')
->withSuffix('swf')
->withFollowSymlinks(true)
->setGenerateChecksums(true);
$raw_files = $finder->find();
echo "Processing ".count($raw_files)." files";
foreach ($raw_files as $path => $hash) {
echo ".";
$path = '/'.Filesystem::readablePath($path, $root);
$type = CelerityResourceTransformer::getResourceType($path);
$hash = md5($hash.$path.$resource_hash);
$uri = '/res/'.substr($hash, 0, 8).$path;
$runtime_map[$path] = array(
'hash' => $hash,
'uri' => $uri,
'disk' => $path,
'type' => $type,
);
}
echo "\n";
$xformer = id(new CelerityResourceTransformer())
->setMinify(false)
->setRawResourceMap($runtime_map);
echo "Finding transformable static resources...\n";
$finder = id(new FileFinder($root))
->withType('f')
->withSuffix('js')
->withSuffix('css')
->withFollowSymlinks(true)
->setGenerateChecksums(true);
$files = $finder->find();
echo "Processing ".count($files)." files";
$file_map = array();
foreach ($files as $path => $raw_hash) {
echo ".";
$path = '/'.Filesystem::readablePath($path, $root);
$data = Filesystem::readFile($root.$path);
$data = $xformer->transformResource($path, $data);
$hash = md5($data);
$hash = md5($hash.$path.$resource_hash);
$file_map[$path] = array(
'hash' => $hash,
'disk' => $path,
);
}
echo "\n";
$resource_graph = array();
$hash_map = array();
$parser = new PhutilDocblockParser();
foreach ($file_map as $path => $info) {
$type = CelerityResourceTransformer::getResourceType($path);
$data = Filesystem::readFile($root.$info['disk']);
$matches = array();
$ok = preg_match('@/[*][*].*?[*]/@s', $data, $matches);
if (!$ok) {
throw new Exception(
"File {$path} does not have a header doc comment. Encode dependency ".
"data in a header docblock.");
}
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.
continue;
}
if (count($provides) > 1) {
throw new Exception(
"File {$path} must @provide at most one Celerity target.");
}
$provides = reset($provides);
$uri = '/res/'.substr($info['hash'], 0, 8).$path;
$hash_map[$provides] = $info['hash'];
$resource_graph[$provides] = $requires;
$runtime_map[$provides] = array(
'uri' => $uri,
'type' => $type,
'requires' => $requires,
'disk' => $path,
);
}
$celerity_resource_graph = new CelerityResourceGraph();
$celerity_resource_graph->addNodes($resource_graph);
$celerity_resource_graph->setResourceGraph($resource_graph);
$celerity_resource_graph->loadGraph();
foreach ($resource_graph as $provides => $requires) {
$cycle = $celerity_resource_graph->detectCycles($provides);
if ($cycle) {
throw new Exception(
"Cycle detected in resource graph: ". implode($cycle, " => ")
);
}
}
$package_map = array();
foreach ($package_spec as $name => $package) {
$hashes = array();
$type = null;
foreach ($package as $symbol) {
if (empty($hash_map[$symbol])) {
throw new Exception(
"Package specification for '{$name}' includes '{$symbol}', but that ".
"symbol is not defined anywhere.");
}
if ($type === null) {
$type = $runtime_map[$symbol]['type'];
} else {
$ntype = $runtime_map[$symbol]['type'];
if ($type !== $ntype) {
throw new Exception(
"Package specification for '{$name}' mixes resources of type ".
"'{$type}' with resources of type '{$ntype}'. Each package may only ".
"contain one type of resource.");
}
}
$hashes[] = $symbol.':'.$hash_map[$symbol];
}
$key = substr(md5(implode("\n", $hashes)), 0, 8);
$package_map['packages'][$key] = array(
'name' => $name,
'symbols' => $package,
'uri' => '/res/pkg/'.$key.'/'.$name,
'type' => $type,
);
foreach ($package as $symbol) {
$package_map['reverse'][$symbol] = $key;
}
}
ksort($runtime_map);
$runtime_map = var_export($runtime_map, true);
$runtime_map = preg_replace('/\s+$/m', '', $runtime_map);
$runtime_map = preg_replace('/array \(/', 'array(', $runtime_map);
$package_map['packages'] = isort($package_map['packages'], 'name');
ksort($package_map['reverse']);
$package_map = var_export($package_map, true);
$package_map = preg_replace('/\s+$/m', '', $package_map);
$package_map = preg_replace('/array \(/', 'array(', $package_map);
$generated = '@'.'generated';
$resource_map = <<<EOFILE
<?php
/**
* This file is automatically generated. Use 'celerity_mapper.php' to rebuild
* it.
* {$generated}
*/
celerity_register_resource_map({$runtime_map}, {$package_map});
EOFILE;
echo "Writing map...\n";
Filesystem::writeFile($celerity_path, $resource_map);
echo "Done.\n";

File diff suppressed because it is too large Load diff

View file

@ -2408,7 +2408,6 @@ phutil_register_library_map(array(
'_phabricator_time_format' => 'view/viewutils.php', '_phabricator_time_format' => 'view/viewutils.php',
'celerity_generate_unique_node_id' => 'infrastructure/celerity/api.php', 'celerity_generate_unique_node_id' => 'infrastructure/celerity/api.php',
'celerity_get_resource_uri' => 'infrastructure/celerity/api.php', 'celerity_get_resource_uri' => 'infrastructure/celerity/api.php',
'celerity_register_resource_map' => 'infrastructure/celerity/map.php',
'implode_selected_handle_links' => 'applications/phid/handle/view/render.php', 'implode_selected_handle_links' => 'applications/phid/handle/view/render.php',
'javelin_tag' => 'infrastructure/javelin/markup.php', 'javelin_tag' => 'infrastructure/javelin/markup.php',
'phabricator_date' => 'view/viewutils.php', 'phabricator_date' => 'view/viewutils.php',

View file

@ -73,7 +73,6 @@ class AphrontDefaultApplicationConfiguration
return array( return array(
'/res/' => array( '/res/' => array(
'(?:(?P<mtime>[0-9]+)T/)?'. '(?:(?P<mtime>[0-9]+)T/)?'.
'(?P<package>pkg/)?'.
'(?P<hash>[a-f0-9]{8})/'. '(?P<hash>[a-f0-9]{8})/'.
'(?P<path>.+\.(?:css|js|jpg|png|swf|gif))' '(?P<path>.+\.(?:css|js|jpg|png|swf|gif))'
=> 'CelerityPhabricatorResourceController', => 'CelerityPhabricatorResourceController',

View file

@ -39,7 +39,7 @@ final class PhameResourceController extends CelerityResourceController {
$spec = $skin->getSpecification(); $spec = $skin->getSpecification();
$this->root = $spec->getRootDirectory().DIRECTORY_SEPARATOR; $this->root = $spec->getRootDirectory().DIRECTORY_SEPARATOR;
return $this->serveResource($this->name, $package_hash = null); return $this->serveResource($this->name);
} }
protected function buildResourceTransformer() { protected function buildResourceTransformer() {

View file

@ -55,10 +55,8 @@ neessary preprocessing. It uses @{class:CelerityResourceMap} to locate resources
and read packaging rules. and read packaging rules.
The dependency and packaging maps are generated by The dependency and packaging maps are generated by
##scripts/celerity_mapper.php##, which updates ##bin/celerity map##, which updates
##src/__celerity_resource_map__.php##. This file is automatically included and ##resources/celerity/map.php##..
just calls @{function:celerity_register_resource_map} with a large blob of
static data to populate @{class:CelerityResourceMap}.
@{class:CelerityStaticResourceResponse} also manages some Javelin information, @{class:CelerityStaticResourceResponse} also manages some Javelin information,
and @{function:celerity_generate_unique_node_id} uses this metadata to provide and @{function:celerity_generate_unique_node_id} uses this metadata to provide

View file

@ -58,7 +58,7 @@ If you've only changed file content things will generally work even if you
don't, but they might start not working as well in the future if you skip this don't, but they might start not working as well in the future if you skip this
step. step.
The generated file `src/__celerity_resource_map__.php` causes merge conflicts The generated file `resources/celerity/map.php` causes merge conflicts
quite often. They can be resolved by running the Celerity mapper. You can quite often. They can be resolved by running the Celerity mapper. You can
automate this process by running: automate this process by running:

View file

@ -12,7 +12,6 @@ final class CelerityPhabricatorResourceController
private $path; private $path;
private $hash; private $hash;
private $package;
protected function getRootDirectory() { protected function getRootDirectory() {
$root = dirname(phutil_get_library_root('phabricator')); $root = dirname(phutil_get_library_root('phabricator'));
@ -22,15 +21,10 @@ final class CelerityPhabricatorResourceController
public function willProcessRequest(array $data) { public function willProcessRequest(array $data) {
$this->path = $data['path']; $this->path = $data['path'];
$this->hash = $data['hash']; $this->hash = $data['hash'];
$this->package = !empty($data['package']);
} }
public function processRequest() { public function processRequest() {
$package_hash = null; return $this->serveResource($this->path);
if ($this->package) {
$package_hash = $this->hash;
}
return $this->serveResource($this->path, $package_hash);
} }
protected function buildResourceTransformer() { protected function buildResourceTransformer() {

View file

@ -39,8 +39,8 @@ abstract class CelerityResourceController extends PhabricatorController {
$map = CelerityResourceMap::getInstance(); $map = CelerityResourceMap::getInstance();
if ($package_hash) { if ($map->isPackageResource($path)) {
$resource_names = $map->getResourceNamesForPackageHash($package_hash); $resource_names = $map->getResourceNamesForPackageName($path);
if (!$resource_names) { if (!$resource_names) {
return new Aphront404Response(); return new Aphront404Response();
} }

View file

@ -5,36 +5,46 @@
* resources, resource dependencies, and packaging information. You generally do * resources, resource dependencies, and packaging information. You generally do
* not need to invoke it directly; instead, you call higher-level Celerity APIs * not need to invoke it directly; instead, you call higher-level Celerity APIs
* and it uses the resource map to satisfy your requests. * and it uses the resource map to satisfy your requests.
*
* @group celerity
*/ */
final class CelerityResourceMap { final class CelerityResourceMap {
private static $instance; private static $instance;
private $resourceMap;
private $resources;
private $symbolMap;
private $requiresMap;
private $packageMap; private $packageMap;
private $reverseMap; private $nameMap;
private $hashMap;
public function __construct(CelerityResources $resources) {
$this->resources = $resources;
$map = $resources->loadMap();
$this->symbolMap = idx($map, 'symbols', array());
$this->requiresMap = idx($map, 'requires', array());
$this->packageMap = idx($map, 'packages', array());
$this->nameMap = idx($map, 'names', array());
// We derive these reverse maps at runtime.
$this->hashMap = array_flip($this->nameMap);
$this->componentMap = array();
foreach ($this->packageMap as $package_name => $symbols) {
foreach ($symbols as $symbol) {
$this->componentMap[$symbol] = $package_name;
}
}
}
public static function getInstance() { public static function getInstance() {
if (empty(self::$instance)) { if (empty(self::$instance)) {
self::$instance = new CelerityResourceMap(); $resources = new CelerityPhabricatorResources();
$root = phutil_get_library_root('phabricator'); self::$instance = new CelerityResourceMap($resources);
$path = '__celerity_resource_map__.php';
$ok = include_once $root.'/'.$path;
if (!$ok) {
throw new Exception(
"Failed to load Celerity resource map!");
}
} }
return self::$instance; return self::$instance;
} }
public function setResourceMap($resource_map) {
$this->resourceMap = $resource_map;
return $this;
}
public function getPackagedNamesForSymbols(array $symbols) { public function getPackagedNamesForSymbols(array $symbols) {
$resolved = $this->resolveResources($symbols); $resolved = $this->resolveResources($symbols);
return $this->packageResources($resolved); return $this->packageResources($resolved);
@ -53,91 +63,71 @@ final class CelerityResourceMap {
} }
private function resolveResource(array &$map, $symbol) { private function resolveResource(array &$map, $symbol) {
if (empty($this->resourceMap[$symbol])) { if (empty($this->symbolMap[$symbol])) {
throw new Exception( throw new Exception(
"Attempting to resolve unknown Celerity resource, '{$symbol}'."); pht(
'Attempting to resolve unknown resource, "%s".',
$symbol));
} }
$info = $this->resourceMap[$symbol]; $hash = $this->symbolMap[$symbol];
foreach ($info['requires'] as $requires) {
if (!empty($map[$requires])) { $map[$symbol] = $hash;
if (isset($this->requiresMap[$hash])) {
$requires = $this->requiresMap[$hash];
} else {
$requires = array();
}
foreach ($requires as $required_symbol) {
if (!empty($map[$required_symbol])) {
continue; continue;
} }
$this->resolveResource($map, $requires); $this->resolveResource($map, $required_symbol);
} }
$map[$symbol] = $info;
}
public function setPackageMap($package_map) {
$this->packageMap = $package_map;
return $this;
} }
private function packageResources(array $resolved_map) { private function packageResources(array $resolved_map) {
$packaged = array(); $packaged = array();
$handled = array(); $handled = array();
foreach ($resolved_map as $symbol => $info) { foreach ($resolved_map as $symbol => $hash) {
if (isset($handled[$symbol])) { if (isset($handled[$symbol])) {
continue; continue;
} }
if (empty($this->packageMap['reverse'][$symbol])) {
$packaged[$symbol] = $info; if (empty($this->componentMap[$symbol])) {
$packaged[] = $this->hashMap[$hash];
} else { } else {
$package = $this->packageMap['reverse'][$symbol]; $package_name = $this->componentMap[$symbol];
$package_info = $this->packageMap['packages'][$package]; $packaged[] = $package_name;
$packaged[$package_info['name']] = $package_info;
foreach ($package_info['symbols'] as $packaged_symbol) { $package_symbols = $this->packageMap[$package_name];
$handled[$packaged_symbol] = true; foreach ($package_symbols as $package_symbol) {
$handled[$package_symbol] = true;
} }
} }
} }
$names = array(); return $packaged;
foreach ($packaged as $key => $resource) {
if (isset($resource['disk'])) {
$names[] = $resource['disk'];
} else {
$names[] = $key;
}
}
return $names;
} }
public function getResourceDataForName($resource_name) { public function getResourceDataForName($resource_name) {
$root = phutil_get_library_root('phabricator'); return $this->resources->getResourceData($resource_name);
$root = dirname($root).'/webroot/';
return Filesystem::readFile($root.$resource_name);
} }
public function getResourceNamesForPackageHash($package_hash) { public function getResourceNamesForPackageName($package_name) {
$package = idx($this->packageMap['packages'], $package_hash); $package_symbols = idx($this->packageMap, $package_name);
if (!$package) { if (!$package_symbols) {
return null; return null;
} }
$paths = array(); $resource_names = array();
foreach ($package['symbols'] as $symbol) { foreach ($package_symbols as $symbol) {
$paths[] = $this->resourceMap[$symbol]['disk']; $resource_names[] = $this->hashMap[$this->symbolMap[$symbol]];
} }
return $paths; return $resource_names;
}
private function lookupSymbolInformation($symbol) {
return idx($this->resourceMap, $symbol);
}
private function lookupFileInformation($path) {
if (empty($this->reverseMap)) {
$this->reverseMap = array();
foreach ($this->resourceMap as $symbol => $data) {
$data['provides'] = $symbol;
$this->reverseMap[$data['disk']] = $data;
}
}
return idx($this->reverseMap, $path);
} }
@ -148,32 +138,18 @@ final class CelerityResourceMap {
* @return int Epoch timestamp of last resource modification. * @return int Epoch timestamp of last resource modification.
*/ */
public function getModifiedTimeForName($name) { public function getModifiedTimeForName($name) {
$package_hash = null; if ($this->isPackageResource($name)) {
foreach ($this->packageMap['packages'] as $hash => $package) { $names = array();
if ($package['name'] == $name) { foreach ($this->packageMap[$name] as $symbol) {
$package_hash = $hash; $names[] = $this->getResourceNameForSymbol($symbol);
break;
}
}
$root = dirname(phutil_get_library_root('phabricator')).'/webroot';
$mtime = 0;
if ($package_hash) {
$names = $this->getResourceNamesForPackageHash($package_hash);
foreach ($names as $component_name) {
$info = $this->lookupFileInformation($component_name);
if ($info) {
$mtime = max($mtime, (int)filemtime($root.$info['disk']));
}
} }
} else { } else {
$info = $this->lookupFileInformation($name); $names = array($name);
if ($info) {
$root = dirname(phutil_get_library_root('phabricator')).'/webroot';
$mtime = (int)filemtime($root.$info['disk']);
} }
$mtime = 0;
foreach ($names as $name) {
$mtime = max($mtime, $this->resources->getResourceModifiedTime($name));
} }
return $mtime; return $mtime;
@ -185,15 +161,11 @@ final class CelerityResourceMap {
* method is fairly low-level and ignores packaging. * method is fairly low-level and ignores packaging.
* *
* @param string Resource symbol to lookup. * @param string Resource symbol to lookup.
* @return string|null Fully-qualified resource URI, or null if the symbol * @return string|null Resource URI, or null if the symbol is unknown.
* is unknown.
*/ */
public function getURIForSymbol($symbol) { public function getURIForSymbol($symbol) {
$info = $this->lookupSymbolInformation($symbol); $hash = idx($this->symbolMap, $symbol);
if ($info) { return $this->getURIForHash($hash);
return idx($info, 'uri');
}
return null;
} }
@ -202,23 +174,27 @@ final class CelerityResourceMap {
* This method is fairly low-level and ignores packaging. * This method is fairly low-level and ignores packaging.
* *
* @param string Resource name to lookup. * @param string Resource name to lookup.
* @return string|null Fully-qualified resource URI, or null if the name * @return string|null Resource URI, or null if the name is unknown.
* is unknown.
*/ */
public function getURIForName($name) { public function getURIForName($name) {
$info = $this->lookupFileInformation($name); $hash = idx($this->nameMap, $name);
if ($info) { return $this->getURIForHash($hash);
return idx($info, 'uri');
} }
foreach ($this->packageMap['packages'] as $hash => $package) {
if ($package['name'] == $name) {
return $package['uri'];
}
}
/**
* Return the absolute URI for a resource, identified by hash.
* This method is fairly low-level and ignores packaging.
*
* @param string Resource hash to lookup.
* @return string|null Resource URI, or null if the hash is unknown.
*/
private function getURIForHash($hash) {
if ($hash === null) {
return null; return null;
} }
return $this->resources->getResourceURI($hash, $this->hashMap[$hash]);
}
/** /**
@ -229,12 +205,12 @@ final class CelerityResourceMap {
* is unknown. * is unknown.
*/ */
public function getRequiredSymbolsForName($name) { public function getRequiredSymbolsForName($name) {
$info = $this->lookupFileInformation($name); $hash = idx($this->symbolMap, $name);
if ($info) { if ($hash === null) {
return idx($info, 'requires', array());
}
return null; return null;
} }
return idx($this->requiresMap, $hash, array());
}
/** /**
@ -244,12 +220,12 @@ final class CelerityResourceMap {
* @return string|null Resource name, or null if the symbol is unknown. * @return string|null Resource name, or null if the symbol is unknown.
*/ */
public function getResourceNameForSymbol($symbol) { public function getResourceNameForSymbol($symbol) {
$info = $this->lookupSymbolInformation($symbol); $hash = idx($this->symbolMap, $symbol);
if ($info) { return idx($this->hashMap, $hash);
return idx($info, 'disk');
}
return null;
} }
public function isPackageResource($name) {
return isset($this->packageMap[$name]);
}
} }

View file

@ -15,10 +15,17 @@ final class CelerityManagementMapWorkflow
public function execute(PhutilArgumentParser $args) { public function execute(PhutilArgumentParser $args) {
$resources_map = CelerityResources::getAll(); $resources_map = CelerityResources::getAll();
$this->log(
pht(
"Rebuilding %d resource source(s).",
new PhutilNumber(count($resources_map))));
foreach ($resources_map as $name => $resources) { foreach ($resources_map as $name => $resources) {
$this->rebuildResources($resources); $this->rebuildResources($resources);
} }
$this->log(pht("Done."));
return 0; return 0;
} }
@ -29,20 +36,36 @@ final class CelerityManagementMapWorkflow
* @return void * @return void
*/ */
private function rebuildResources(CelerityResources $resources) { private function rebuildResources(CelerityResources $resources) {
$this->log(
pht(
'Rebuilding resource source "%s" (%s)...',
$resources->getName(),
get_class($resources)));
$binary_map = $this->rebuildBinaryResources($resources); $binary_map = $this->rebuildBinaryResources($resources);
$this->log(
pht(
'Found %d binary resources.',
new PhutilNumber(count($binary_map))));
$xformer = id(new CelerityResourceTransformer()) $xformer = id(new CelerityResourceTransformer())
->setMinify(false) ->setMinify(false)
->setRawURIMap(ipull($binary_map, 'uri')); ->setRawURIMap(ipull($binary_map, 'uri'));
$text_map = $this->rebuildTextResources($resources, $xformer); $text_map = $this->rebuildTextResources($resources, $xformer);
$this->log(
pht(
'Found %d text resources.',
new PhutilNumber(count($text_map))));
$resource_graph = array(); $resource_graph = array();
$requires_map = array(); $requires_map = array();
$provides_map = array(); $symbol_map = array();
foreach ($text_map as $name => $info) { foreach ($text_map as $name => $info) {
if (isset($info['provides'])) { if (isset($info['provides'])) {
$provides_map[$info['provides']] = $info['hash']; $symbol_map[$info['provides']] = $info['hash'];
// We only need to check for cycles and add this to the requires map // We only need to check for cycles and add this to the requires map
// if it actually requires anything. // if it actually requires anything.
@ -54,15 +77,49 @@ final class CelerityManagementMapWorkflow
} }
$this->detectGraphCycles($resource_graph); $this->detectGraphCycles($resource_graph);
$name_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash');
$hash_map = array_flip($name_map);
$hash_map = ipull($binary_map, 'hash') + ipull($text_map, 'hash'); $package_map = $this->rebuildPackages(
$resources,
$symbol_map,
$hash_map);
$this->log(
pht(
'Found %d packages.',
new PhutilNumber(count($package_map))));
// TODO: Actually do things. $component_map = array();
foreach ($package_map as $package_name => $package_info) {
foreach ($package_info['symbols'] as $symbol) {
$component_map[$symbol] = $package_name;
}
}
var_dump($provides_map); $name_map = $this->mergeNameMaps(
var_dump($requires_map); array(
var_dump($hash_map); 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);
} }
@ -202,4 +259,112 @@ final class CelerityManagementMapWorkflow
} }
} }
/**
* Build package specifications for a given resource source.
*
* @param CelerityResources 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(
CelerityResources $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) {
$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;
}
} }

View file

@ -1,13 +0,0 @@
<?php
/**
* Registers a resource map for Celerity. This is glue code between the Celerity
* mapper script and @{class:CelerityResourceMap}.
*
* @group celerity
*/
function celerity_register_resource_map(array $map, array $package_map) {
$instance = CelerityResourceMap::getInstance();
$instance->setResourceMap($map);
$instance->setPackageMap($package_map);
}

View file

@ -21,4 +21,8 @@ final class CelerityPhabricatorResources extends CelerityResourcesOnDisk {
return dirname(phutil_get_library_root('phabricator')).'/'.$to_file; return dirname(phutil_get_library_root('phabricator')).'/'.$to_file;
} }
public function getResourcePackages() {
return include $this->getPhabricatorPath('resources/celerity/packages.php');
}
} }

View file

@ -5,11 +5,14 @@
*/ */
abstract class CelerityResources { abstract class CelerityResources {
private $map;
abstract public function getName(); abstract public function getName();
abstract public function getPathToMap(); abstract public function getPathToMap();
abstract public function getResourceData($name); abstract public function getResourceData($name);
abstract public function findBinaryResources(); abstract public function findBinaryResources();
abstract public function findTextResources(); abstract public function findTextResources();
abstract public function getResourceModifiedTime($name);
public function getCelerityHash($data) { public function getCelerityHash($data) {
$tail = PhabricatorEnv::getEnvConfig('celerity.resource-hash'); $tail = PhabricatorEnv::getEnvConfig('celerity.resource-hash');
@ -25,6 +28,17 @@ abstract class CelerityResources {
return "/res/{$hash}/{$name}"; return "/res/{$hash}/{$name}";
} }
public function getResourcePackages() {
return array();
}
public function loadMap() {
if ($this->map === null) {
$this->map = include $this->getPathToMap();
}
return $this->map;
}
public static function getAll() { public static function getAll() {
static $resources_map; static $resources_map;
if ($resources_map === null) { if ($resources_map === null) {

View file

@ -7,8 +7,12 @@ abstract class CelerityResourcesOnDisk extends CelerityResources {
abstract public function getPathToResources(); abstract public function getPathToResources();
private function getPathToResource($name) {
return $this->getPathToResources().DIRECTORY_SEPARATOR.$name;
}
public function getResourceData($name) { public function getResourceData($name) {
return Filesystem::readFile($this->getPathToResources().'/'.$name); return Filesystem::readFile($this->getPathToResource($name));
} }
public function findBinaryResources() { public function findBinaryResources() {
@ -19,6 +23,10 @@ abstract class CelerityResourcesOnDisk extends CelerityResources {
return $this->findResourcesWithSuffixes($this->getTextFileSuffixes()); return $this->findResourcesWithSuffixes($this->getTextFileSuffixes());
} }
public function getResourceModifiedTime($name) {
return (int)filemtime($this->getPathToResource($name));
}
protected function getBinaryFileSuffixes() { protected function getBinaryFileSuffixes() {
return array( return array(
'png', 'png',