diff --git a/lib/private/Template/functions.php b/lib/private/Template/functions.php
index 402a7491e035d..326b5e578e7ee 100644
--- a/lib/private/Template/functions.php
+++ b/lib/private/Template/functions.php
@@ -54,23 +54,22 @@ function emit_css_loading_tags($obj): void {
* @param string $src the source URL, ignored when empty
* @param string $script_content the inline script content, ignored when empty
* @param string $content_type the type of the source (e.g. 'module')
+ *
+ * @since 27.0.0 added the $content_type parameter
*/
function emit_script_tag(string $src, string $script_content = '', string $content_type = ''): void {
$nonceManager = Server::get(ContentSecurityPolicyNonceManager::class);
- $defer_str = ' defer';
+ $defer_str = $content_type === '' ? ' defer' : ''; // "defer" only works with classic scripts
$type = $content_type !== '' ? ' type="' . $content_type . '"' : '';
- $s = '';
print_unescaped($s . "\n");
@@ -81,6 +80,8 @@ function emit_script_tag(string $src, string $script_content = '', string $conte
* @param array $obj all the script information from template
*/
function emit_script_loading_tags($obj): void {
+ emit_import_map($obj);
+
foreach ($obj['jsfiles'] as $jsfile) {
$fileName = explode('?', $jsfile, 2)[0];
$type = str_ends_with($fileName, '.mjs') ? 'module' : '';
@@ -91,6 +92,29 @@ function emit_script_loading_tags($obj): void {
}
}
+/**
+ * Print the import map for the current JS modules.
+ * The import map is needed to ensure that an import of an entry point does not duplicate the state,
+ * but reuses the already loaded module. This is needed because Nextcloud will append a cache buster
+ * to the entry point URLs but the scripts does not know about that (both must match).
+ *
+ * @param $obj all the script information from template
+ */
+function emit_import_map(array $obj): void {
+ $modules = [];
+ foreach ($obj['jsfiles'] as $jsfile) {
+ $fileName = explode('?', $jsfile, 2)[0];
+ if (str_ends_with($fileName, '.mjs') && $jsfile !== $fileName) {
+ // its a module and we have a cache buster available
+ $modules[$fileName] = $jsfile;
+ }
+ }
+ if (!empty($modules)) {
+ $json = json_encode(['imports' => $modules], JSON_UNESCAPED_SLASHES | JSON_FORCE_OBJECT);
+ emit_script_tag('', $json, 'importmap');
+ }
+}
+
/**
* Prints an unsanitized string - usage of this function may result into XSS.
* Consider using p() instead.
diff --git a/tests/lib/TemplateFunctionsTest.php b/tests/lib/TemplateFunctionsTest.php
index 8c1523628ab51..3afb1e4e34355 100644
--- a/tests/lib/TemplateFunctionsTest.php
+++ b/tests/lib/TemplateFunctionsTest.php
@@ -54,13 +54,34 @@ public function testEmitScriptTagWithSource(): void {
}
public function testEmitScriptTagWithModuleSource(): void {
- $this->expectOutputRegex('/