diff --git a/scripts/__init_script__.php b/scripts/__init_script__.php new file mode 100644 index 0000000000..a69ac9a5a8 --- /dev/null +++ b/scripts/__init_script__.php @@ -0,0 +1,32 @@ +\n"; + exit(1); +} + +phutil_require_module('phutil', 'filesystem'); +phutil_require_module('phutil', 'filesystem/filefinder'); +phutil_require_module('phutil', 'future/exec'); +phutil_require_module('phutil', 'parser/docblock'); + +$root = Filesystem::resolvePath($argv[1]); + +echo "Finding static resources...\n"; +$files = id(new FileFinder($root)) + ->withType('f') + ->withSuffix('js') + ->withSuffix('css') + ->setGenerateChecksums(true) + ->find(); + +echo "Processing ".count($files)." files"; + +$file_map = array(); +foreach ($files as $path => $hash) { + echo "."; + $name = '/'.Filesystem::readablePath($path, $root); + $file_map[$name] = array( + 'hash' => $hash, + 'disk' => $path, + ); +} +echo "\n"; + +$runtime_map = array(); + +$parser = new PhutilDocblockParser(); +foreach ($file_map as $path => $info) { + $data = Filesystem::readFile($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 (count($provides) !== 1) { + throw new Exception( + "File {$path} must @provide exactly one Celerity target."); + } + + $provides = reset($provides); + + $type = 'js'; + if (preg_match('/\.css$/', $path)) { + $type = 'css'; + } + + $path = '/res/'.substr($info['hash'], 0, 8).$path; + + $runtime_map[$provides] = array( + 'path' => $path, + 'type' => $type, + 'requires' => $requires, + ); +} + +$runtime_map = var_export($runtime_map, true); +$runtime_map = preg_replace('/\s+$/m', '', $runtime_map); +$runtime_map = preg_replace('/array \(/', 'array(', $runtime_map); + +$resource_map = << + array( + 'path' => '/res/ffa0140c/rsrc/css/base.css', + 'type' => 'css', + 'requires' => + array( + ), + ), + 'phabricator-syntax-css' => + array( + 'path' => '/res/bf911307/rsrc/css/syntax.css', + 'type' => 'css', + 'requires' => + array( + ), + ), + 'javelin-init-dev' => + array( + 'path' => '/res/c57a9e89/rsrc/js/javelin/init.dev.js', + 'type' => 'js', + 'requires' => + array( + ), + ), + 'javelin-init-prod' => + array( + 'path' => '/res/f0172c54/rsrc/js/javelin/init.min.js', + 'type' => 'js', + 'requires' => + array( + ), + ), + 'javelin-lib-dev' => + array( + 'path' => '/res/3e747182/rsrc/js/javelin/javelin.dev.js', + 'type' => 'js', + 'requires' => + array( + ), + ), + 'javelin-lib-prod' => + array( + 'path' => '/res/9438670e/rsrc/js/javelin/javelin.min.js', + 'type' => 'js', + 'requires' => + array( + ), + ), +)); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 13f5a717a0..e663af492f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -46,6 +46,10 @@ phutil_register_library_map(array( 'AphrontURIMapper' => 'aphront/mapper', 'AphrontView' => 'view/base', 'AphrontWebpageResponse' => 'aphront/response/webpage', + 'CelerityAPI' => 'infratructure/celerity/api', + 'CelerityResourceController' => 'infratructure/celerity/controller', + 'CelerityResourceMap' => 'infratructure/celerity/map', + 'CelerityStaticResourceResponse' => 'infratructure/celerity/response', 'ConduitAPIMethod' => 'applications/conduit/method/base', 'ConduitAPIRequest' => 'applications/conduit/protocol/request', 'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect', @@ -121,10 +125,12 @@ phutil_register_library_map(array( array( '_qsprintf_check_scalar_type' => 'storage/qsprintf', '_qsprintf_check_type' => 'storage/qsprintf', + 'celerity_register_resource_map' => 'infratructure/celerity/map', 'qsprintf' => 'storage/qsprintf', 'queryfx' => 'storage/queryfx', 'queryfx_all' => 'storage/queryfx', 'queryfx_one' => 'storage/queryfx', + 'require_celerity_resource' => 'infratructure/celerity/api', 'vqsprintf' => 'storage/qsprintf', 'vqueryfx' => 'storage/queryfx', 'xsprintf_query' => 'storage/qsprintf', @@ -161,6 +167,7 @@ phutil_register_library_map(array( 'AphrontSideNavView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontWebpageResponse' => 'AphrontResponse', + 'CelerityResourceController' => 'AphrontController', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 8aa024d12f..d6f7168334 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -81,6 +81,11 @@ class AphrontDefaultApplicationConfiguration 'changeset/(?\d+)/$' => 'DifferentialChangesetViewController', ), + '/res/' => array( + '(?[a-f0-9]{8})/(?[^.]+\.(?:css|js))$' + => 'CelerityResourceController', + ), + '.*' => 'AphrontDefaultApplicationController', ); } diff --git a/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php b/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php index 6c5e4c2188..314c3f91b6 100644 --- a/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php +++ b/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php @@ -96,7 +96,7 @@ final class DifferentialDiffTableOfContentsView extends AphrontView { $rows[] = ''. - ''.$char.''. + ''.$char.''. ''.$pchar.''. ''.$desc.''. ''.$link.$lines.''. diff --git a/src/infratructure/celerity/api/CelerityAPI.php b/src/infratructure/celerity/api/CelerityAPI.php new file mode 100644 index 0000000000..77ba73f10c --- /dev/null +++ b/src/infratructure/celerity/api/CelerityAPI.php @@ -0,0 +1,35 @@ +requireResource($symbol); +} diff --git a/src/infratructure/celerity/api/__init__.php b/src/infratructure/celerity/api/__init__.php new file mode 100644 index 0000000000..c05435461a --- /dev/null +++ b/src/infratructure/celerity/api/__init__.php @@ -0,0 +1,12 @@ +path = $data['path']; + $this->hash = $data['hash']; + } + + public function processRequest() { + $path = $this->path; + + // Sanity checking to keep this from exposing anything sensitive. + $path = preg_replace('@(//|\\.\\.)@', '', $path); + $matches = null; + if (!preg_match('/\.(css|js)$/', $path, $matches)) { + throw new Exception("Only CSS and JS resources may be served."); + } + + $type = $matches[1]; + + + $root = dirname(phutil_get_library_root('phabricator')); + + try { + $data = Filesystem::readFile($root.'/webroot/'.$path); + } catch (Exception $ex) { + return new Aphront404Response(); + } + + $response = new AphrontFileResponse(); + $response->setContent($data); + switch ($type) { + case 'css': + $response->setMimeType("text/css; charset=utf-8"); + break; + case 'js': + $response->setMimeType("text/javascript; charset=utf-8"); + break; + } + + return $response; + } + +} diff --git a/src/infratructure/celerity/controller/__init__.php b/src/infratructure/celerity/controller/__init__.php new file mode 100644 index 0000000000..c31e65c785 --- /dev/null +++ b/src/infratructure/celerity/controller/__init__.php @@ -0,0 +1,16 @@ +resourceMap = $resource_map; + return $this; + } + + public function resolveResources(array $symbols) { + $map = array(); + foreach ($symbols as $symbol) { + if (!empty($map[$symbol])) { + continue; + } + $this->resolveResource($map, $symbol); + } + + return $map; + } + + private function resolveResource(array &$map, $symbol) { + if (empty($this->resourceMap[$symbol])) { + throw new Exception( + "Attempting to resolve unknown resource, '{$symbol}'."); + } + + $info = $this->resourceMap[$symbol]; + foreach ($info['requires'] as $requires) { + if (!empty($map[$requires])) { + continue; + } + $this->resolveResource($map, $requires); + } + + $map[$symbol] = $info; + } + +} + +function celerity_register_resource_map(array $map) { + $instance = CelerityResourceMap::getInstance(); + $instance->setResourceMap($map); +} diff --git a/src/infratructure/celerity/map/__init__.php b/src/infratructure/celerity/map/__init__.php new file mode 100644 index 0000000000..e2362e7274 --- /dev/null +++ b/src/infratructure/celerity/map/__init__.php @@ -0,0 +1,12 @@ +symbols[$symbol] = true; + $this->needsResolve = true; + return $this; + } + + private function resolveResources() { + if ($this->needsResolve) { + $map = CelerityResourceMap::getInstance(); + $this->resolved = $map->resolveResources(array_keys($this->symbols)); + $this->needsResolve = false; + } + return $this; + } + + public function renderResourcesOfType($type) { + $this->resolveResources(); + $output = array(); + foreach ($this->resolved as $resource) { + if ($resource['type'] == $type) { + $output[] = $this->renderResource($resource); + } + } + return implode("\n", $output); + } + + private function renderResource(array $resource) { + switch ($resource['type']) { + case 'css': + $path = phutil_escape_html($resource['path']); + return ''; + case 'js': + $path = phutil_escape_html($resource['path']); + return ''. ''; @@ -102,20 +111,18 @@ class PhabricatorStandardPageView extends AphrontPageView { phutil_escape_html($this->getApplicationName())). $tabs. ''. - $this->renderChildren(). + $this->bodyContent. '
'. ''; } protected function getTail() { - return - ''. + $response = CelerityAPI::getStaticResourceResponse(); + return + $response->renderResourcesOfType('js'). ''; - - ; } } diff --git a/src/view/page/standard/__init__.php b/src/view/page/standard/__init__.php index d931d61dd5..2d2bef01bd 100644 --- a/src/view/page/standard/__init__.php +++ b/src/view/page/standard/__init__.php @@ -6,6 +6,7 @@ +phutil_require_module('phabricator', 'infratructure/celerity/api'); phutil_require_module('phabricator', 'view/page/base'); phutil_require_module('phutil', 'markup'); diff --git a/webroot/rsrc/css/base.css b/webroot/rsrc/css/base.css index dedfd4699e..c5033f27d6 100644 --- a/webroot/rsrc/css/base.css +++ b/webroot/rsrc/css/base.css @@ -1,3 +1,7 @@ +/** + * @provides phabricator-core-css + */ + html { overflow-y: scroll; } diff --git a/webroot/rsrc/css/syntax.css b/webroot/rsrc/css/syntax.css index 706a864d0f..dfa773dfd7 100644 --- a/webroot/rsrc/css/syntax.css +++ b/webroot/rsrc/css/syntax.css @@ -1,3 +1,7 @@ +/** + * @provides phabricator-syntax-css + */ + .remarkup-code .uu { /* Forbidden Unicode */ color: #aa0066; } diff --git a/webroot/rsrc/js/javelin/init.dev.js b/webroot/rsrc/js/javelin/init.dev.js index 38c386e7fb..eadfd68a8e 100644 --- a/webroot/rsrc/js/javelin/init.dev.js +++ b/webroot/rsrc/js/javelin/init.dev.js @@ -1,3 +1,4 @@ +/** @provides javelin-init-dev */ /** * Javelin core; installs Javelin and Stratcom event delegation. * diff --git a/webroot/rsrc/js/javelin/init.min.js b/webroot/rsrc/js/javelin/init.min.js index af32f431c8..264f75659c 100644 --- a/webroot/rsrc/js/javelin/init.min.js +++ b/webroot/rsrc/js/javelin/init.min.js @@ -1 +1,2 @@ +/** @provides javelin-init-prod */ (function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})(); \ No newline at end of file diff --git a/webroot/rsrc/js/javelin/javelin.dev.js b/webroot/rsrc/js/javelin/javelin.dev.js index ad7e5cfdd1..7f877a124f 100644 --- a/webroot/rsrc/js/javelin/javelin.dev.js +++ b/webroot/rsrc/js/javelin/javelin.dev.js @@ -1,3 +1,5 @@ +/** @provides javelin-lib-dev */ + /** * Javelin utility functions. * diff --git a/webroot/rsrc/js/javelin/javelin.min.js b/webroot/rsrc/js/javelin/javelin.min.js index 60bb82e21f..e199548c52 100644 --- a/webroot/rsrc/js/javelin/javelin.min.js +++ b/webroot/rsrc/js/javelin/javelin.min.js @@ -1 +1,2 @@ +/** @provides javelin-lib-prod */ JX.$A=function(b){var c=[];for(var a=0;a=300){this._z();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._z();return;}try{if(response.error){this._z(response.error);}else{JX.Stratcom.mergeData(this._v,response.javelin_metadata||{});this._zb(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_z:function(a){this._za();this.invoke('error',a,this);this.invoke('finally');},_zb:function(b){this._za();if(b.onload)for(var a=0;a-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&').replace(/"/g,'"').replace(//g,'>');},show:function(){for(var a=0;a')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a