From ad295ca9975e90c500d9e642a0d773cc1cd08d05 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Wed, 8 Apr 2015 21:20:04 -0700 Subject: [PATCH] CRM-16145 - When aggregating JS, dedupe adjacent closures. The reorganized Angular files have added ~50 identical closures, and there are still other big modules which remain to split. Ideally, the raw JS files wouldn't bother with these closures, but we'd have to rework the debug use-case and karma use-case if we removed the closures from the original file (and that most likely means a change to developer workflow). This patch accomplishes the goal without changing developer workflow. --- CRM/Utils/JS.php | 43 ++++++++++++++++++++++++++++++ Civi/Angular/Page/Modules.php | 20 +++++++++++++- tests/phpunit/CRM/Utils/JSTest.php | 43 ++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/CRM/Utils/JS.php b/CRM/Utils/JS.php index d33af229a2..c006cf8ea2 100644 --- a/CRM/Utils/JS.php +++ b/CRM/Utils/JS.php @@ -65,4 +65,47 @@ class CRM_Utils_JS { return array_values($strings); } + /** + * Identify duplicate, adjacent, identical closures and consolidate them. + * + * Note that you can only dedupe closures if they are directly adjacent and + * have exactly the same parameters. + * + * @param array $scripts + * Javascript source. + * @param array $localVars + * Ordered list of JS vars to identify the start of a closure. + * @param array $inputVals + * Ordered list of input values passed into the closure. + * @return string + * Javascript source. + */ + public static function dedupeClosures($scripts, $localVars, $inputVals) { + // Example opening: (function (angular, $, _) { + $opening = '\s*\(\s*function\s*\(\s*'; + $opening .= implode(',\s*', array_map(function ($v) { + return preg_quote($v, '/'); + }, $localVars)); + $opening .= '\)\s*\{'; + $opening = '/^' . $opening . '/'; + + // Example closing: })(angular, CRM.$, CRM._); + $closing = '\}\s*\)\s*\(\s*'; + $closing .= implode(',\s*', array_map(function ($v) { + return preg_quote($v, '/'); + }, $inputVals)); + $closing .= '\);\s*'; + $closing = "/$closing\$/"; + + $scripts = array_values($scripts); + for ($i = count($scripts) - 1; $i > 0; $i--) { + if (preg_match($closing, $scripts[$i - 1]) && preg_match($opening, $scripts[$i])) { + $scripts[$i - 1] = preg_replace($closing, '', $scripts[$i - 1]); + $scripts[$i] = preg_replace($opening, '', $scripts[$i]); + } + } + + return $scripts; + } + } diff --git a/Civi/Angular/Page/Modules.php b/Civi/Angular/Page/Modules.php index 70a7a20837..0cd9744a5c 100644 --- a/Civi/Angular/Page/Modules.php +++ b/Civi/Angular/Page/Modules.php @@ -41,7 +41,7 @@ class Modules extends \CRM_Core_Page { case 'js': $this->send( 'application/javascript', - \CRM_Utils_File::concat($angular->getResources($moduleNames, 'js', 'path'), "\n") + $this->digestJs($angular->getResources($moduleNames, 'js', 'path')) ); break; @@ -59,6 +59,24 @@ class Modules extends \CRM_Core_Page { \CRM_Utils_System::civiExit(); } + /** + * @param array $files + * File paths. + * @return string + */ + public function digestJs($files) { + $scripts = array(); + foreach ($files as $file) { + $scripts[] = file_get_contents($file); + } + $scripts = \CRM_Utils_JS::dedupeClosures( + $scripts, + array('angular', '$', '_'), + array('angular', 'CRM.$', 'CRM._') + ); + return implode("\n", $scripts); + } + /** * @param string $modulesExpr * Comma-separated list of module names. diff --git a/tests/phpunit/CRM/Utils/JSTest.php b/tests/phpunit/CRM/Utils/JSTest.php index d420f9a82f..05d2487ff9 100644 --- a/tests/phpunit/CRM/Utils/JSTest.php +++ b/tests/phpunit/CRM/Utils/JSTest.php @@ -110,4 +110,47 @@ class CRM_Utils_JSTest extends CiviUnitTestCase { $this->assertEquals($expectedStrings, $actualStrings); } + public function dedupeClosureExamples() { + // Each example string here is named for its body, eg the body of $a calls "a()". + $a = "(function (angular, $, _) {\na();\n})(angular, CRM.$, CRM._);"; + $b = "(function(angular,$,_){\nb();\n})(angular,CRM.$,CRM._);"; + $c = "(function( angular, $,_) {\nc();\n})(angular,CRM.$, CRM._);"; + $d = "(function (angular, $, _, whiz) {\nd();\n})(angular, CRM.$, CRM._, CRM.whizbang);"; + $m = "alert('i is the trickster (function( angular, $,_) {\nm();\n})(angular,CRM.$, CRM._);)'"; + // Note: $d has a fundamentally different closure. + + // Each example string here is a deduped combination of others, + // eg "$ab" is the deduping of $a+$b. + $ab = "(function (angular, $, _) {\na();\n\nb();\n})(angular,CRM.$,CRM._);"; + $abc = "(function (angular, $, _) {\na();\n\nb();\n\nc();\n})(angular,CRM.$, CRM._);"; + $cb = "(function( angular, $,_) {\nc();\n\nb();\n})(angular,CRM.$,CRM._);"; + + $cases = array(); + $cases[] = array(array($a), "$a"); + $cases[] = array(array($b), "$b"); + $cases[] = array(array($c), "$c"); + $cases[] = array(array($d), "$d"); + $cases[] = array(array($m), "$m"); + $cases[] = array(array($a, $b), "$ab"); + $cases[] = array(array($a, $m, $b), "$a$m$b"); + $cases[] = array(array($a, $d), "$a$d"); + $cases[] = array(array($a, $d, $b), "$a$d$b"); + $cases[] = array(array($a, $b, $c), "$abc"); + $cases[] = array(array($a, $b, $d, $c, $b), "$ab$d$cb"); + return $cases; + } + + /** + * @param array $scripts + * @param string $expectedOutput + * @dataProvider dedupeClosureExamples + */ + public function testDedupeClosure($scripts, $expectedOutput) { + $actualOutput = CRM_Utils_JS::dedupeClosures( + $scripts, + array('angular', '$', '_'), + array('angular', 'CRM.$', 'CRM._') + ); + $this->assertEquals($expectedOutput, implode("", $actualOutput)); + } } -- 2.25.1