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
* @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];
......
......@@ -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']])) {
......
.. 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
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);
}
}
......@@ -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']);
}
}
......@@ -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);
}
......
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