[FEATURE] Support nomodule for JavaScript includes 08/58908/13
authorGuido Schmechel <guido.schmechel@brandung.de>
Fri, 16 Nov 2018 22:42:53 +0000 (23:42 +0100)
committerSusanne Moog <look@susi.dev>
Sat, 9 Nov 2019 12:57:20 +0000 (13:57 +0100)
Add the property 'nomodule="nomodule"' to JavaScript files via TypoScript
page.includeJSlibs.<array>.nomodule = 1

This patch affects the TypoScript PAGE properties
* includeJSlibs
* includeJSFooterlibs
* includeJS
* includeJSFooter

Resolves: #86759
Releases: master
Change-Id: I88db40ac973c17148a87504998f1070eba8d74af
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/58908
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Tested-by: Susanne Moog <look@susi.dev>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Susanne Moog <look@susi.dev>
typo3/sysext/core/Classes/Page/PageRenderer.php
typo3/sysext/core/Classes/Resource/ResourceCompressor.php
typo3/sysext/core/Documentation/Changelog/master/Feature-86759-SupportNomoduleAttributeForJavaScriptIncludes.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Page/PageRendererTest.php
typo3/sysext/core/Tests/Unit/Resource/ResourceCompressorTest.php
typo3/sysext/frontend/Classes/Http/RequestHandler.php

index 249344c..363f4f7 100644 (file)
@@ -1018,8 +1018,9 @@ class PageRenderer implements SingletonInterface
      * @param string $integrity Subresource Integrity (SRI)
      * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
      * @param string $crossorigin CORS settings attribute
+     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
      */
-    public function addJsLibrary($name, $file, $type = '', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
+    public function addJsLibrary($name, $file, $type = 'text/javascript', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
     {
         if (!in_array(strtolower($name), $this->jsLibs)) {
             $this->jsLibs[strtolower($name)] = [
@@ -1035,6 +1036,7 @@ class PageRenderer implements SingletonInterface
                 'integrity' => $integrity,
                 'defer' => $defer,
                 'crossorigin' => $crossorigin,
+                'nomodule' => $nomodule,
             ];
         }
     }
@@ -1054,8 +1056,9 @@ class PageRenderer implements SingletonInterface
      * @param string $integrity Subresource Integrity (SRI)
      * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
      * @param string $crossorigin CORS settings attribute
+     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
      */
-    public function addJsFooterLibrary($name, $file, $type = '', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
+    public function addJsFooterLibrary($name, $file, $type = 'text/javascript', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
     {
         $name .= '_jsFooterLibrary';
         if (!in_array(strtolower($name), $this->jsLibs)) {
@@ -1072,6 +1075,7 @@ class PageRenderer implements SingletonInterface
                 'integrity' => $integrity,
                 'defer' => $defer,
                 'crossorigin' => $crossorigin,
+                'nomodule' => $nomodule,
             ];
         }
     }
@@ -1090,8 +1094,9 @@ class PageRenderer implements SingletonInterface
      * @param string $integrity Subresource Integrity (SRI)
      * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
      * @param string $crossorigin CORS settings attribute
+     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
      */
-    public function addJsFile($file, $type = '', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
+    public function addJsFile($file, $type = 'text/javascript', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
     {
         if (!isset($this->jsFiles[$file])) {
             $this->jsFiles[$file] = [
@@ -1107,6 +1112,7 @@ class PageRenderer implements SingletonInterface
                 'integrity' => $integrity,
                 'defer' => $defer,
                 'crossorigin' => $crossorigin,
+                'nomodule' => $nomodule,
             ];
         }
     }
@@ -1125,8 +1131,9 @@ class PageRenderer implements SingletonInterface
      * @param string $integrity Subresource Integrity (SRI)
      * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
      * @param string $crossorigin CORS settings attribute
+     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
      */
-    public function addJsFooterFile($file, $type = '', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '')
+    public function addJsFooterFile($file, $type = 'text/javascript', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
     {
         if (!isset($this->jsFiles[$file])) {
             $this->jsFiles[$file] = [
@@ -1142,6 +1149,7 @@ class PageRenderer implements SingletonInterface
                 'integrity' => $integrity,
                 'defer' => $defer,
                 'crossorigin' => $crossorigin,
+                'nomodule' => $nomodule,
             ];
         }
     }
@@ -2116,9 +2124,10 @@ class PageRenderer implements SingletonInterface
                 $properties['file'] = $this->getStreamlinedFileName($properties['file']);
                 $async = $properties['async'] ? ' async="async"' : '';
                 $defer = $properties['defer'] ? ' defer="defer"' : '';
+                $nomodule = $properties['nomodule'] ? ' nomodule="nomodule"' : '';
                 $integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
                 $crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
-                $tag = '<script src="' . htmlspecialchars($properties['file']) . '" type="' . htmlspecialchars($properties['type']) . '"' . $async . $defer . $integrity . $crossorigin . '></script>';
+                $tag = '<script src="' . htmlspecialchars($properties['file']) . '" type="' . htmlspecialchars($properties['type']) . '"' . $async . $defer . $integrity . $crossorigin . $nomodule . '></script>';
                 if ($properties['allWrap']) {
                     $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
                     $tag = $wrapArr[0] . $tag . $wrapArr[1];
@@ -2161,9 +2170,10 @@ class PageRenderer implements SingletonInterface
                 $type = $properties['type'] ? ' type="' . htmlspecialchars($properties['type']) . '"' : '';
                 $async = $properties['async'] ? ' async="async"' : '';
                 $defer = $properties['defer'] ? ' defer="defer"' : '';
+                $nomodule = $properties['nomodule'] ? ' nomodule="nomodule"' : '';
                 $integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
                 $crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
-                $tag = '<script src="' . htmlspecialchars($file) . '"' . $type . $async . $defer . $integrity . $crossorigin . '></script>';
+                $tag = '<script src="' . htmlspecialchars($file) . '"' . $type . $async . $defer . $integrity . $crossorigin . $nomodule . '></script>';
                 if ($properties['allWrap']) {
                     $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
                     $tag = $wrapArr[0] . $tag . $wrapArr[1];
index 2d0dae8..f45f685 100644 (file)
@@ -160,7 +160,7 @@ class ResourceCompressor
         $filesToInclude = [];
         foreach ($jsFiles as $key => $fileOptions) {
             // invalid section found or no concatenation allowed, so continue
-            if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation'])) {
+            if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation']) || !empty($fileOptions['nomodule'])) {
                 continue;
             }
             if (!isset($filesToInclude[$fileOptions['section']])) {
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-86759-SupportNomoduleAttributeForJavaScriptIncludes.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-86759-SupportNomoduleAttributeForJavaScriptIncludes.rst
new file mode 100644 (file)
index 0000000..8a370f6
--- /dev/null
@@ -0,0 +1,22 @@
+.. include:: ../../Includes.txt
+
+====================================================================
+Feature: #86759 - Support nomodule attribute for JavaScript includes
+====================================================================
+
+See :issue:`86759`
+
+Description
+===========
+
+When including JavaScript files in TypoScript, the HTML5 attribute :html:`nomodule` is now
+supported.
+
+See https://html.spec.whatwg.org/multipage/scripting.html#attr-script-nomodule
+
+.. code-block:: typoscript
+
+   page.includeJSFooter.file = path/to/file.js
+   page.includeJSFooter.file.nomodule = 1
+
+.. index:: TypoScript
\ No newline at end of file
index 87d5c28..513d8b6 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 namespace TYPO3\CMS\Core\Tests\Functional\Page;
 
 /*
@@ -68,7 +69,16 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
         $headerData = $expectedHeaderData = '<tag method="private" name="test" />';
         $subject->addHeaderData($headerData);
 
-        $subject->addJsLibrary('test', 'fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
+        $subject->addJsLibrary(
+            'test',
+            'fileadmin/test.js',
+            'text/javascript',
+            false,
+            false,
+            'wrapBeforeXwrapAfter',
+            false,
+            'X'
+        );
         $expectedJsLibraryRegExp = '#wrapBefore<script src="fileadmin/test\\.(js|\\d+\\.js|js\\?\\d+)" type="text/javascript"></script>wrapAfter#';
 
         $subject->addJsFile('fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
@@ -133,10 +143,27 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
         $subject->addFooterData($footerData);
 
         $expectedJsFooterLibraryRegExp = '#wrapBefore<script src="fileadmin/test\\.(js|\\d+\\.js|js\\?\\d+)" type="text/javascript"></script>wrapAfter#';
-        $subject->addJsFooterLibrary('test', 'fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
+        $subject->addJsFooterLibrary(
+            'test',
+            'fileadmin/test.js',
+            'text/javascript',
+            false,
+            false,
+            'wrapBeforeXwrapAfter',
+            false,
+            'X'
+        );
 
         $expectedJsFooterRegExp = '#wrapBefore<script src="fileadmin/test\\.(js|\\d+\\.js|js\\?\\d+)" type="text/javascript"></script>wrapAfter#';
-        $subject->addJsFooterFile('fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
+        $subject->addJsFooterFile(
+            'fileadmin/test.js',
+            'text/javascript',
+            false,
+            false,
+            'wrapBeforeXwrapAfter',
+            false,
+            'X'
+        );
 
         $jsFooterInlineCode = $expectedJsFooterInlineCodeString = 'var x = "' . $this->getUniqueId('jsFooterInline-') . '"';
         $subject->addJsFooterInlineCode($this->getUniqueId(), $jsFooterInlineCode);
@@ -178,4 +205,87 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
         self::assertStringContainsString($expectedLanguageLabel2, $renderedString);
         self::assertStringContainsString($expectedInlineSettingsReturnValue, $renderedString);
     }
+
+    /**
+     * @test
+     */
+    public function pageRendererRendersNomoduleJavascript()
+    {
+        $subject = new PageRenderer();
+        $subject->setCharSet('utf-8');
+        $subject->setLanguage('default');
+
+        $subject->addJsFooterLibrary(
+            'test',
+            'fileadmin/test.js',
+            'text/javascript',
+            false,
+            false,
+            '',
+            false,
+            '|',
+            false,
+            '',
+            false,
+            '',
+            true
+        );
+        $expectedJsFooterLibrary = '<script src="fileadmin/test.js" type="text/javascript" nomodule="nomodule"></script>';
+
+        $subject->addJsLibrary(
+            'test2',
+            'fileadmin/test2.js',
+            'text/javascript',
+            false,
+            false,
+            '',
+            false,
+            '|',
+            false,
+            '',
+            false,
+            '',
+            true
+        );
+        $expectedJsLibrary = '<script src="fileadmin/test2.js" type="text/javascript" nomodule="nomodule"></script>';
+
+        $subject->addJsFile(
+            'fileadmin/test3.js',
+            'text/javascript',
+            false,
+            false,
+            '',
+            false,
+            '|',
+            false,
+            '',
+            false,
+            '',
+            true
+        );
+        $expectedJsFile = '<script src="fileadmin/test3.js" type="text/javascript" nomodule="nomodule"></script>';
+
+        $subject->addJsFooterFile(
+            'fileadmin/test4.js',
+            'text/javascript',
+            false,
+            false,
+            '',
+            false,
+            '|',
+            false,
+            '',
+            false,
+            '',
+            true
+        );
+        $expectedJsFooter = '<script src="fileadmin/test4.js" type="text/javascript" nomodule="nomodule"></script>';
+
+        $renderedString = $subject->render();
+
+        self::assertStringContainsString($expectedJsFooterLibrary, $renderedString);
+        self::assertStringContainsString($expectedJsLibrary, $renderedString);
+        self::assertStringContainsString($expectedJsFile, $renderedString);
+        self::assertStringContainsString($expectedJsFooter, $renderedString);
+    }
 }
index ed6ec64..6a01900 100644 (file)
@@ -641,4 +641,25 @@ class ResourceCompressorTest extends BaseTestCase
         $relativeToRootPath = $subject->_call('getFilenameFromMainDir', $filename);
         self::assertSame($expected, $relativeToRootPath);
     }
+
+    /**
+     * @test
+     */
+    public function nomoduleJavascriptIsNotConcatenated(): void
+    {
+        $fileName = 'fooFile.js';
+        $concatenatedFileName = 'merged_' . $fileName;
+        $testFileFixture = [
+            $fileName => [
+                'file' => $fileName,
+                'nomodule' => true,
+                'section' => 'top',
+            ]
+        ];
+
+        $result = $this->subject->concatenateJsFiles($testFileFixture);
+
+        self::assertArrayNotHasKey($concatenatedFileName, $result);
+        self::assertTrue($result[$fileName]['nomodule']);
+    }
 }
index e4c8d7b..b9776be 100644 (file)
@@ -517,7 +517,8 @@ class RequestHandler implements RequestHandlerInterface
                             (bool)$jsFileConfig['async'],
                             $jsFileConfig['integrity'],
                             (bool)$jsFileConfig['defer'],
-                            $crossOrigin
+                            $crossOrigin,
+                            (bool)$jsFileConfig['nomodule']
                         );
                         unset($jsFileConfig);
                     }
@@ -558,7 +559,8 @@ class RequestHandler implements RequestHandlerInterface
                             (bool)$jsFileConfig['async'],
                             $jsFileConfig['integrity'],
                             (bool)$jsFileConfig['defer'],
-                            $crossorigin
+                            $crossorigin,
+                            (bool)$jsFileConfig['nomodule']
                         );
                         unset($jsFileConfig);
                     }
@@ -599,7 +601,8 @@ class RequestHandler implements RequestHandlerInterface
                             (bool)$jsConfig['async'],
                             $jsConfig['integrity'],
                             (bool)$jsConfig['defer'],
-                            $crossorigin
+                            $crossorigin,
+                            (bool)$jsConfig['nomodule']
                         );
                         unset($jsConfig);
                     }
@@ -639,7 +642,8 @@ class RequestHandler implements RequestHandlerInterface
                             (bool)$jsConfig['async'],
                             $jsConfig['integrity'],
                             (bool)$jsConfig['defer'],
-                            $crossorigin
+                            $crossorigin,
+                            (bool)$jsConfig['nomodule']
                         );
                         unset($jsConfig);
                     }