Commit 3fa18fab authored by Guido Schmechel's avatar Guido Schmechel Committed by Susanne Moog
Browse files

[FEATURE] Support nomodule for JavaScript includes

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: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Daniel Goerz's avatarDaniel Goerz <daniel.goerz@posteo.de>
Tested-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
Reviewed-by: Daniel Goerz's avatarDaniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
parent 3fde1b1d
...@@ -1018,8 +1018,9 @@ class PageRenderer implements SingletonInterface ...@@ -1018,8 +1018,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI) * @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute * @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)) { if (!in_array(strtolower($name), $this->jsLibs)) {
$this->jsLibs[strtolower($name)] = [ $this->jsLibs[strtolower($name)] = [
...@@ -1035,6 +1036,7 @@ class PageRenderer implements SingletonInterface ...@@ -1035,6 +1036,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity, 'integrity' => $integrity,
'defer' => $defer, 'defer' => $defer,
'crossorigin' => $crossorigin, 'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
]; ];
} }
} }
...@@ -1054,8 +1056,9 @@ class PageRenderer implements SingletonInterface ...@@ -1054,8 +1056,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI) * @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute * @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'; $name .= '_jsFooterLibrary';
if (!in_array(strtolower($name), $this->jsLibs)) { if (!in_array(strtolower($name), $this->jsLibs)) {
...@@ -1072,6 +1075,7 @@ class PageRenderer implements SingletonInterface ...@@ -1072,6 +1075,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity, 'integrity' => $integrity,
'defer' => $defer, 'defer' => $defer,
'crossorigin' => $crossorigin, 'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
]; ];
} }
} }
...@@ -1090,8 +1094,9 @@ class PageRenderer implements SingletonInterface ...@@ -1090,8 +1094,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI) * @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute * @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])) { if (!isset($this->jsFiles[$file])) {
$this->jsFiles[$file] = [ $this->jsFiles[$file] = [
...@@ -1107,6 +1112,7 @@ class PageRenderer implements SingletonInterface ...@@ -1107,6 +1112,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity, 'integrity' => $integrity,
'defer' => $defer, 'defer' => $defer,
'crossorigin' => $crossorigin, 'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
]; ];
} }
} }
...@@ -1125,8 +1131,9 @@ class PageRenderer implements SingletonInterface ...@@ -1125,8 +1131,9 @@ class PageRenderer implements SingletonInterface
* @param string $integrity Subresource Integrity (SRI) * @param string $integrity Subresource Integrity (SRI)
* @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
* @param string $crossorigin CORS settings attribute * @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])) { if (!isset($this->jsFiles[$file])) {
$this->jsFiles[$file] = [ $this->jsFiles[$file] = [
...@@ -1142,6 +1149,7 @@ class PageRenderer implements SingletonInterface ...@@ -1142,6 +1149,7 @@ class PageRenderer implements SingletonInterface
'integrity' => $integrity, 'integrity' => $integrity,
'defer' => $defer, 'defer' => $defer,
'crossorigin' => $crossorigin, 'crossorigin' => $crossorigin,
'nomodule' => $nomodule,
]; ];
} }
} }
...@@ -2116,9 +2124,10 @@ class PageRenderer implements SingletonInterface ...@@ -2116,9 +2124,10 @@ class PageRenderer implements SingletonInterface
$properties['file'] = $this->getStreamlinedFileName($properties['file']); $properties['file'] = $this->getStreamlinedFileName($properties['file']);
$async = $properties['async'] ? ' async="async"' : ''; $async = $properties['async'] ? ' async="async"' : '';
$defer = $properties['defer'] ? ' defer="defer"' : ''; $defer = $properties['defer'] ? ' defer="defer"' : '';
$nomodule = $properties['nomodule'] ? ' nomodule="nomodule"' : '';
$integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : ''; $integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
$crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : ''; $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']) { if ($properties['allWrap']) {
$wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2); $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
$tag = $wrapArr[0] . $tag . $wrapArr[1]; $tag = $wrapArr[0] . $tag . $wrapArr[1];
...@@ -2161,9 +2170,10 @@ class PageRenderer implements SingletonInterface ...@@ -2161,9 +2170,10 @@ class PageRenderer implements SingletonInterface
$type = $properties['type'] ? ' type="' . htmlspecialchars($properties['type']) . '"' : ''; $type = $properties['type'] ? ' type="' . htmlspecialchars($properties['type']) . '"' : '';
$async = $properties['async'] ? ' async="async"' : ''; $async = $properties['async'] ? ' async="async"' : '';
$defer = $properties['defer'] ? ' defer="defer"' : ''; $defer = $properties['defer'] ? ' defer="defer"' : '';
$nomodule = $properties['nomodule'] ? ' nomodule="nomodule"' : '';
$integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : ''; $integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
$crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : ''; $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']) { if ($properties['allWrap']) {
$wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2); $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
$tag = $wrapArr[0] . $tag . $wrapArr[1]; $tag = $wrapArr[0] . $tag . $wrapArr[1];
......
...@@ -160,7 +160,7 @@ class ResourceCompressor ...@@ -160,7 +160,7 @@ class ResourceCompressor
$filesToInclude = []; $filesToInclude = [];
foreach ($jsFiles as $key => $fileOptions) { foreach ($jsFiles as $key => $fileOptions) {
// invalid section found or no concatenation allowed, so continue // 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; continue;
} }
if (!isset($filesToInclude[$fileOptions['section']])) { if (!isset($filesToInclude[$fileOptions['section']])) {
......
.. 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
<?php <?php
namespace TYPO3\CMS\Core\Tests\Functional\Page; namespace TYPO3\CMS\Core\Tests\Functional\Page;
/* /*
...@@ -68,7 +69,16 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona ...@@ -68,7 +69,16 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
$headerData = $expectedHeaderData = '<tag method="private" name="test" />'; $headerData = $expectedHeaderData = '<tag method="private" name="test" />';
$subject->addHeaderData($headerData); $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#'; $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'); $subject->addJsFile('fileadmin/test.js', 'text/javascript', false, false, 'wrapBeforeXwrapAfter', false, 'X');
...@@ -133,10 +143,27 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona ...@@ -133,10 +143,27 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
$subject->addFooterData($footerData); $subject->addFooterData($footerData);
$expectedJsFooterLibraryRegExp = '#wrapBefore<script src="fileadmin/test\\.(js|\\d+\\.js|js\\?\\d+)" type="text/javascript"></script>wrapAfter#'; $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#'; $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-') . '"'; $jsFooterInlineCode = $expectedJsFooterInlineCodeString = 'var x = "' . $this->getUniqueId('jsFooterInline-') . '"';
$subject->addJsFooterInlineCode($this->getUniqueId(), $jsFooterInlineCode); $subject->addJsFooterInlineCode($this->getUniqueId(), $jsFooterInlineCode);
...@@ -178,4 +205,87 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona ...@@ -178,4 +205,87 @@ class PageRendererTest extends \TYPO3\TestingFramework\Core\Functional\Functiona
self::assertStringContainsString($expectedLanguageLabel2, $renderedString); self::assertStringContainsString($expectedLanguageLabel2, $renderedString);
self::assertStringContainsString($expectedInlineSettingsReturnValue, $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);
}
} }
...@@ -641,4 +641,25 @@ class ResourceCompressorTest extends BaseTestCase ...@@ -641,4 +641,25 @@ class ResourceCompressorTest extends BaseTestCase
$relativeToRootPath = $subject->_call('getFilenameFromMainDir', $filename); $relativeToRootPath = $subject->_call('getFilenameFromMainDir', $filename);
self::assertSame($expected, $relativeToRootPath); 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']);
}
} }
...@@ -517,7 +517,8 @@ class RequestHandler implements RequestHandlerInterface ...@@ -517,7 +517,8 @@ class RequestHandler implements RequestHandlerInterface
(bool)$jsFileConfig['async'], (bool)$jsFileConfig['async'],
$jsFileConfig['integrity'], $jsFileConfig['integrity'],
(bool)$jsFileConfig['defer'], (bool)$jsFileConfig['defer'],
$crossOrigin $crossOrigin,
(bool)$jsFileConfig['nomodule']
); );
unset($jsFileConfig); unset($jsFileConfig);
} }
...@@ -558,7 +559,8 @@ class RequestHandler implements RequestHandlerInterface ...@@ -558,7 +559,8 @@ class RequestHandler implements RequestHandlerInterface
(bool)$jsFileConfig['async'], (bool)$jsFileConfig['async'],
$jsFileConfig['integrity'], $jsFileConfig['integrity'],
(bool)$jsFileConfig['defer'], (bool)$jsFileConfig['defer'],
$crossorigin $crossorigin,
(bool)$jsFileConfig['nomodule']
); );
unset($jsFileConfig); unset($jsFileConfig);
} }
...@@ -599,7 +601,8 @@ class RequestHandler implements RequestHandlerInterface ...@@ -599,7 +601,8 @@ class RequestHandler implements RequestHandlerInterface
(bool)$jsConfig['async'], (bool)$jsConfig['async'],
$jsConfig['integrity'], $jsConfig['integrity'],
(bool)$jsConfig['defer'], (bool)$jsConfig['defer'],
$crossorigin $crossorigin,
(bool)$jsConfig['nomodule']
); );
unset($jsConfig); unset($jsConfig);
} }
...@@ -639,7 +642,8 @@ class RequestHandler implements RequestHandlerInterface ...@@ -639,7 +642,8 @@ class RequestHandler implements RequestHandlerInterface
(bool)$jsConfig['async'], (bool)$jsConfig['async'],
$jsConfig['integrity'], $jsConfig['integrity'],
(bool)$jsConfig['defer'], (bool)$jsConfig['defer'],
$crossorigin $crossorigin,
(bool)$jsConfig['nomodule']
); );
unset($jsConfig); unset($jsConfig);
} }
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment