From 09576ce717d62173cd49d44ac3611550fabb3314 Mon Sep 17 00:00:00 2001
From: Rune Piper <kontakt@runepiper.de>
Date: Wed, 6 Jun 2018 09:22:21 +0200
Subject: [PATCH] [FEATURE] Load merged JS files asynchronous

The async attribute is now assigned to the script tag of the concatenated
JS files if all files have the async attribute enabled in TypoScript.

Resolves: #83476
Releases: master
Change-Id: If4d5f03cac5920cf0bcccefb2e91cc229f9b9e77
Reviewed-on: https://review.typo3.org/57130
Reviewed-by: Sascha Egerer <sascha@sascha-egerer.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
---
 .../Classes/Resource/ResourceCompressor.php   |  10 +-
 ...re-83476-LoadMergedJSFilesAsynchronous.rst |  30 ++++
 .../Unit/Resource/ResourceCompressorTest.php  | 129 +++++++++++++++++-
 3 files changed, 167 insertions(+), 2 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-83476-LoadMergedJSFilesAsynchronous.rst

diff --git a/typo3/sysext/core/Classes/Resource/ResourceCompressor.php b/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
index 093adea2b830..c24d4f5f9a3e 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceCompressor.php
@@ -169,6 +169,8 @@ class ResourceCompressor
      */
     public function concatenateJsFiles(array $jsFiles)
     {
+        $concatenatedJsFileIsAsync = false;
+        $allFilesToConcatenateAreAsync = true;
         $filesToInclude = [];
         foreach ($jsFiles as $key => $fileOptions) {
             // invalid section found or no concatenation allowed, so continue
@@ -184,6 +186,11 @@ class ResourceCompressor
             } else {
                 $filesToInclude[$fileOptions['section']][] = $filenameFromMainDir;
             }
+            if (!empty($fileOptions['async']) && (bool)$fileOptions['async']) {
+                $concatenatedJsFileIsAsync = true;
+            } else {
+                $allFilesToConcatenateAreAsync = false;
+            }
             // remove the file from the incoming file array
             unset($jsFiles[$key]);
         }
@@ -197,7 +204,8 @@ class ResourceCompressor
                     'compress' => true,
                     'excludeFromConcatenation' => true,
                     'forceOnTop' => false,
-                    'allWrap' => ''
+                    'allWrap' => '',
+                    'async' => $concatenatedJsFileIsAsync && $allFilesToConcatenateAreAsync,
                 ];
                 // place the merged javascript on top of the JS files
                 $jsFiles = array_merge([$targetFile => $concatenatedOptions], $jsFiles);
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-83476-LoadMergedJSFilesAsynchronous.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-83476-LoadMergedJSFilesAsynchronous.rst
new file mode 100644
index 000000000000..da24a12e1cd8
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-83476-LoadMergedJSFilesAsynchronous.rst
@@ -0,0 +1,30 @@
+.. include:: ../../Includes.txt
+
+===================================================
+Feature: #83476 - Load merged JS files asynchronous
+===================================================
+
+See :issue:`83476`
+
+Description
+===========
+
+The async attribute is now assigned to the script tag of the concatenated JS files if all files have the async attribute enabled in TypoScript.
+
+Example:
+--------
+
+.. code-block:: typoscript
+
+   config.concatenateJs = 1
+
+   page = PAGE
+   page.includeJSFooter {
+       test = fileadmin/user_upload/test.js
+       test.async = 1
+
+       test2 = fileadmin/user_upload/test2.js
+       test2.async = 1
+   }
+
+.. index:: Frontend, TypoScript, ext:core
diff --git a/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorTest.php b/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorTest.php
index 97f12a3661da..4c873a1be4fd 100644
--- a/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorTest.php
+++ b/typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorTest.php
@@ -287,7 +287,7 @@ class ResourceCompressorTest extends BaseTestCase
     /**
      * @test
      */
-    public function concatenatedJsFileIsFlaggedToNotConcatenateAgain(): void
+    public function concatenateJsFileIsFlaggedToNotConcatenateAgain(): void
     {
         $fileName = 'fooFile.js';
         $concatenatedFileName = 'merged_' . $fileName;
@@ -309,6 +309,133 @@ class ResourceCompressorTest extends BaseTestCase
         $this->assertTrue($result[$concatenatedFileName]['excludeFromConcatenation']);
     }
 
+    /**
+     * @return array
+     */
+    public function concatenateJsFileAsyncDataProvider(): array
+    {
+        return [
+            'all files have no async' => [
+                [
+                    [
+                        'file' => 'file1.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                    ],
+                    [
+                        'file' => 'file2.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                    ],
+                ],
+                false
+            ],
+            'all files have async false' => [
+                [
+                    [
+                        'file' => 'file1.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => false,
+                    ],
+                    [
+                        'file' => 'file2.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => false,
+                    ],
+                ],
+                false
+            ],
+            'all files have async true' => [
+                [
+                    [
+                        'file' => 'file1.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => true,
+                    ],
+                    [
+                        'file' => 'file2.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => true,
+                    ],
+                ],
+                true
+            ],
+            'one file async true and one file async false' => [
+                [
+                    [
+                        'file' => 'file1.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => true,
+                    ],
+                    [
+                        'file' => 'file2.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => false,
+                    ],
+                ],
+                false
+            ],
+            'one file async true and one file async false but is excluded form concatenation' => [
+                [
+                    [
+                        'file' => 'file1.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => true,
+                    ],
+                    [
+                        'file' => 'file2.js',
+                        'excludeFromConcatenation' => true,
+                        'section' => 'top',
+                        'async' => false,
+                    ],
+                ],
+                true
+            ],
+            'one file async false and one file async true but is excluded form concatenation' => [
+                [
+                    [
+                        'file' => 'file1.js',
+                        'excludeFromConcatenation' => false,
+                        'section' => 'top',
+                        'async' => false,
+                    ],
+                    [
+                        'file' => 'file2.js',
+                        'excludeFromConcatenation' => true,
+                        'section' => 'top',
+                        'async' => true,
+                    ],
+                ],
+                false
+            ],
+        ];
+    }
+
+    /**
+     * @test
+     * @dataProvider concatenateJsFileAsyncDataProvider
+     * @param string $input
+     * @param bool $expected
+     */
+    public function concatenateJsFileAddsAsyncPropertyIfAllFilesAreAsync(array $input, bool $expected): void
+    {
+        $concatenatedFileName = 'merged_foo.js';
+        $this->subject->expects($this->once())
+            ->method('createMergedJsFile')
+            ->will($this->returnValue($concatenatedFileName));
+
+        $result = $this->subject->concatenateJsFiles($input);
+
+        $this->assertSame($expected, $result[$concatenatedFileName]['async']);
+    }
+
     /**
      * @return array
      */
-- 
GitLab