diff --git a/typo3/sysext/core/Classes/Resource/ResourceCompressor.php b/typo3/sysext/core/Classes/Resource/ResourceCompressor.php index 093adea2b83088d71191de1df7593b4feec63cc6..c24d4f5f9a3eb9049ba2c1ac4d79e7b180d03155 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 0000000000000000000000000000000000000000..da24a12e1cd8c740f360df598393d28bf705923b --- /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 97f12a3661dac74b7d455d76b615a1c4f30129d7..4c873a1be4fd2825c7342b84cad45c219e5907c5 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 */