CRM-16145 - When aggregating JS, dedupe adjacent closures.
authorTim Otten <totten@civicrm.org>
Thu, 9 Apr 2015 04:20:04 +0000 (21:20 -0700)
committerTim Otten <totten@civicrm.org>
Thu, 9 Apr 2015 05:31:07 +0000 (22:31 -0700)
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
Civi/Angular/Page/Modules.php
tests/phpunit/CRM/Utils/JSTest.php

index d33af229a26364bc01586f401004ed0c68084d91..c006cf8ea201798af106abdcccf70823747f6234 100644 (file)
@@ -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;
+  }
+
 }
index 70a7a20837a90b32624d6b71d68456f53e36844d..0cd9744a5cbd421bebdccfd59f803adf85b8f6d3 100644 (file)
@@ -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.
index d420f9a82fbeadabf9b374cca4cd596cd8e57a64..05d2487ff9b88f70814652b95acc7a5e8964ef6e 100644 (file)
@@ -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));
+  }
 }