From f4a9f49e4fdb77b9b88dc5b5db8c0af5dac61c7c Mon Sep 17 00:00:00 2001 From: Fedik Date: Sat, 3 Jun 2023 17:59:56 +0300 Subject: [PATCH 01/11] EsmImportMap --- .../Renderer/Html/ScriptsRenderer.php | 7 +++ libraries/src/WebAsset/WebAssetManager.php | 48 +++++++++++++++++++ templates/cassiopeia/index.php | 5 ++ 3 files changed, 60 insertions(+) diff --git a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php index add08b04bcd6d..294ee30a3785f 100644 --- a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php +++ b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php @@ -51,6 +51,7 @@ public function render($head, $params = [], $content = null) $buffer = ''; $wam = $this->_doc->getWebAssetManager(); $assets = $wam->getAssets('script', true); + $importmap = $wam->getEsmImportMap(); // Get a list of inline assets and their relation with regular assets $inlineAssets = $wam->filterOutInlineAssets($assets); @@ -59,6 +60,12 @@ public function render($head, $params = [], $content = null) // Merge with existing scripts, for rendering $assets = array_merge(array_values($assets), $this->_doc->_scripts); + // Start with importmap if any + if ($importmap) { + $jsonImports = json_encode($importmap, JDEBUG ? JSON_PRETTY_PRINT : false); + $buffer .= $tab . ''; + } + // Generate script file links foreach ($assets as $key => $item) { // Check whether we have an Asset instance, or old array with attributes diff --git a/libraries/src/WebAsset/WebAssetManager.php b/libraries/src/WebAsset/WebAssetManager.php index a354d82edf2f5..3506b32f82f1b 100644 --- a/libraries/src/WebAsset/WebAssetManager.php +++ b/libraries/src/WebAsset/WebAssetManager.php @@ -88,6 +88,15 @@ class WebAssetManager implements WebAssetManagerInterface */ protected $activeAssets = []; + /** + * Import map for ES modules. + * + * @var array + * + * @since __DEPLOY_VERSION__ + */ + protected $esmImportMap = []; + /** * Internal marker to check the manager state, * to prevent use of the manager after an assets are rendered @@ -166,6 +175,7 @@ public function reset(): WebAssetManagerInterface } $this->activeAssets = []; + $this->esmImportMap = []; $this->dependenciesIsActual = false; return $this; @@ -692,6 +702,44 @@ public function addInline(string $type, $content, array $options = [], array $at return $this; } + /** + * Add element to ESM importmap + * + * @param string $name A module name + * @param string $value The path to file or dir + * @param string $scope An optional scope + * + * @return self + * + * @since __DEPLOY_VERSION__ + */ + public function addEsmImport(string $name, string $value, string $scope = ''): self + { + if ($this->locked) { + throw new InvalidActionException('WebAssetManager is locked'); + } + + if (!$scope) { + $this->esmImportMap['imports'][$name] = $value; + } else { + $this->esmImportMap['scopes'][$scope][$name] = $value; + } + + return $this; + } + + /** + * Return importmap, including the map from active assets + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public function getEsmImportMap(): array + { + return $this->esmImportMap; + } + /** * Lock the manager to prevent further modifications * diff --git a/templates/cassiopeia/index.php b/templates/cassiopeia/index.php index 0ead32b485aeb..1315507b1a182 100644 --- a/templates/cassiopeia/index.php +++ b/templates/cassiopeia/index.php @@ -110,6 +110,11 @@ // Defer fontawesome for increased performance. Once the page is loaded javascript changes it to a stylesheet. $wa->getAsset('style', 'fontawesome')->setAttribute('rel', 'lazy-stylesheet'); + +$wa->addEsmImport('test', './test.js'); +$wa->addEsmImport('test', './test2.js', '/en/'); + +dump($wa->getEsmImportMap()); ?> From d2b99e4a7a393560173e9ab8beeb1fc6a916794c Mon Sep 17 00:00:00 2001 From: Fedik Date: Sat, 3 Jun 2023 19:33:12 +0300 Subject: [PATCH 02/11] EsmImportMap --- .../Renderer/Html/ScriptsRenderer.php | 61 ++++++++++++++++--- libraries/src/WebAsset/WebAssetItem.php | 5 ++ libraries/src/WebAsset/WebAssetManager.php | 48 --------------- templates/cassiopeia/index.php | 7 ++- 4 files changed, 62 insertions(+), 59 deletions(-) diff --git a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php index 294ee30a3785f..3637f4baa6c06 100644 --- a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php +++ b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php @@ -51,21 +51,17 @@ public function render($head, $params = [], $content = null) $buffer = ''; $wam = $this->_doc->getWebAssetManager(); $assets = $wam->getAssets('script', true); - $importmap = $wam->getEsmImportMap(); // Get a list of inline assets and their relation with regular assets $inlineAssets = $wam->filterOutInlineAssets($assets); $inlineRelation = $wam->getInlineRelation($inlineAssets); + // Generate importmap first + $buffer .= $this->renderImportmap($assets); + // Merge with existing scripts, for rendering $assets = array_merge(array_values($assets), $this->_doc->_scripts); - // Start with importmap if any - if ($importmap) { - $jsonImports = json_encode($importmap, JDEBUG ? JSON_PRETTY_PRINT : false); - $buffer .= $tab . ''; - } - // Generate script file links foreach ($assets as $key => $item) { // Check whether we have an Asset instance, or old array with attributes @@ -145,7 +141,7 @@ private function renderElement($item): string $src = $asset ? $asset->getUri() : ($item['src'] ?? ''); // Make sure we have a src, and it not already rendered - if (!$src || !empty($this->renderedSrc[$src]) || ($asset && $asset->getOption('webcomponent'))) { + if (!$src || !empty($this->renderedSrc[$src]) || ($asset && $asset->getOption('importmapOnly'))) { return ''; } @@ -327,4 +323,53 @@ private function renderAttributes(array $attributes): string return $buffer; } + + /** + * Renders ESM importmap element + * + * @param WebAssetItemInterface[] $assets The assets list + * + * @return string The attributes string + * + * @since __DEPLOY_VERSION__ + */ + private function renderImportmap(array $assets) + { + $buffer = ''; + $importmap = ['imports' => []]; + $tab = $this->_doc->_getTab(); + $mediaVersion = $this->_doc->getMediaVersion(); + + // Collect a modules for the map + foreach ($assets as $item) { + // Only type=module can be mapped + if ($item->getAttribute('type') !== 'module') { + continue; + } + + $esmName = $item->getOption('importmapName') ?: $item->getName(); + $esmScope = $item->getOption('importmapScope'); + $version = $item->getVersion(); + $src = $item->getUri(); + + // Check if script uses media version. + if ($version && !str_contains($src, '?') && !str_ends_with($src, '/') && ($mediaVersion || $version !== 'auto')) { + $src .= '?' . ($version === 'auto' ? $mediaVersion : $version); + } + + if (!$esmScope) { + $importmap['imports'][$esmName] = $src; + } else { + $importmap['scopes'][$esmScope][$esmName] = $src; + } + } + + // Render map + if (!empty($importmap['imports'])) { + $jsonImports = json_encode($importmap, JDEBUG ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES); + $buffer .= $tab . ''; + } + + return $buffer; + } } diff --git a/libraries/src/WebAsset/WebAssetItem.php b/libraries/src/WebAsset/WebAssetItem.php index d46286c31b6cd..aef3cd6e63d75 100644 --- a/libraries/src/WebAsset/WebAssetItem.php +++ b/libraries/src/WebAsset/WebAssetItem.php @@ -10,6 +10,7 @@ namespace Joomla\CMS\WebAsset; use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Uri\Uri; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; @@ -175,6 +176,10 @@ public function getUri($resolvePath = true): string $path = $this->resolvePath($path, 'stylesheet'); break; default: + // Asset for the ES modules may give us a folder for ESM import map + if (str_ends_with($path, '/') && !str_starts_with($path, '.')) { + $path = Uri::root(true) . '/' . $path; + } break; } } diff --git a/libraries/src/WebAsset/WebAssetManager.php b/libraries/src/WebAsset/WebAssetManager.php index 3506b32f82f1b..a354d82edf2f5 100644 --- a/libraries/src/WebAsset/WebAssetManager.php +++ b/libraries/src/WebAsset/WebAssetManager.php @@ -88,15 +88,6 @@ class WebAssetManager implements WebAssetManagerInterface */ protected $activeAssets = []; - /** - * Import map for ES modules. - * - * @var array - * - * @since __DEPLOY_VERSION__ - */ - protected $esmImportMap = []; - /** * Internal marker to check the manager state, * to prevent use of the manager after an assets are rendered @@ -175,7 +166,6 @@ public function reset(): WebAssetManagerInterface } $this->activeAssets = []; - $this->esmImportMap = []; $this->dependenciesIsActual = false; return $this; @@ -702,44 +692,6 @@ public function addInline(string $type, $content, array $options = [], array $at return $this; } - /** - * Add element to ESM importmap - * - * @param string $name A module name - * @param string $value The path to file or dir - * @param string $scope An optional scope - * - * @return self - * - * @since __DEPLOY_VERSION__ - */ - public function addEsmImport(string $name, string $value, string $scope = ''): self - { - if ($this->locked) { - throw new InvalidActionException('WebAssetManager is locked'); - } - - if (!$scope) { - $this->esmImportMap['imports'][$name] = $value; - } else { - $this->esmImportMap['scopes'][$scope][$name] = $value; - } - - return $this; - } - - /** - * Return importmap, including the map from active assets - * - * @return array - * - * @since __DEPLOY_VERSION__ - */ - public function getEsmImportMap(): array - { - return $this->esmImportMap; - } - /** * Lock the manager to prevent further modifications * diff --git a/templates/cassiopeia/index.php b/templates/cassiopeia/index.php index 1315507b1a182..dd3d2713c0cb4 100644 --- a/templates/cassiopeia/index.php +++ b/templates/cassiopeia/index.php @@ -111,10 +111,11 @@ // Defer fontawesome for increased performance. Once the page is loaded javascript changes it to a stylesheet. $wa->getAsset('style', 'fontawesome')->setAttribute('rel', 'lazy-stylesheet'); -$wa->addEsmImport('test', './test.js'); -$wa->addEsmImport('test', './test2.js', '/en/'); +//$wa->addEsmImport('test', './test.js'); +//$wa->addEsmImport('test', './test2.js', '/en/'); +$wa->useScript('test1'); -dump($wa->getEsmImportMap()); +//dump($wa->getEsmImportMap()); ?> From 0deae1fe07036c3a1e6de02d07d6897ea5061c69 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sun, 4 Jun 2023 10:39:55 +0300 Subject: [PATCH 03/11] EsmImportMap --- libraries/src/Document/Renderer/Html/ScriptsRenderer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php index 3637f4baa6c06..3238086b23b78 100644 --- a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php +++ b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php @@ -342,8 +342,8 @@ private function renderImportmap(array $assets) // Collect a modules for the map foreach ($assets as $item) { - // Only type=module can be mapped - if ($item->getAttribute('type') !== 'module') { + // Only importmap:true can be mapped + if (!$item->getOption('importmap')) { continue; } From 5ab52f88539e53331ad5c298d7187b98308ffff5 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sun, 4 Jun 2023 10:56:40 +0300 Subject: [PATCH 04/11] EsmImportMap --- build/media_source/system/joomla.asset.json | 8 ++++++ build/media_source/system/js/esm-map.js | 27 +++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 build/media_source/system/js/esm-map.js diff --git a/build/media_source/system/joomla.asset.json b/build/media_source/system/joomla.asset.json index ec9194a0f72d6..21cbfda4f087d 100644 --- a/build/media_source/system/joomla.asset.json +++ b/build/media_source/system/joomla.asset.json @@ -36,6 +36,14 @@ "type": "module" } }, + { + "name": "esm-map", + "type": "script", + "uri": "system/esm-map.min.js", + "attributes": { + "type": "module" + } + }, { "name": "messages-legacy", "type": "script", diff --git a/build/media_source/system/js/esm-map.js b/build/media_source/system/js/esm-map.js new file mode 100644 index 0000000000000..3a47d46f2e7e0 --- /dev/null +++ b/build/media_source/system/js/esm-map.js @@ -0,0 +1,27 @@ +/** + * @copyright (C) 2018 Open Source Matters, Inc. + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +let map; +/** + * Resolve module name to it's URL when "importmap" is not supported, + * otherwise return module name unchanged. + * + * Usage example: + * const { foo } = await import(joomlaESMap('@bar-module')); + * or + * import(joomlaESMap('@bar-module')).then(({ foo }) => { ... }); + * + * @param {String} name Module name + * @param {boolean} force Force to resolve, even when "importmap" is supported + * @returns {String} + */ +window.joomlaESMap = (name, force) => { + if (HTMLScriptElement.supports('importmap') && !force) return name; + if (!map) { + const m = document.querySelector('script[type="importmap"]'); + map = m ? JSON.parse(m.textContent) : {imports: {}}; + } + return map.imports[name] || name; +}; From 2f0c0f121813d6cce7a85824c337786999274db1 Mon Sep 17 00:00:00 2001 From: Fedik Date: Sun, 4 Jun 2023 11:23:02 +0300 Subject: [PATCH 05/11] EsmImportMap --- templates/cassiopeia/index.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/templates/cassiopeia/index.php b/templates/cassiopeia/index.php index dd3d2713c0cb4..0ead32b485aeb 100644 --- a/templates/cassiopeia/index.php +++ b/templates/cassiopeia/index.php @@ -110,12 +110,6 @@ // Defer fontawesome for increased performance. Once the page is loaded javascript changes it to a stylesheet. $wa->getAsset('style', 'fontawesome')->setAttribute('rel', 'lazy-stylesheet'); - -//$wa->addEsmImport('test', './test.js'); -//$wa->addEsmImport('test', './test2.js', '/en/'); -$wa->useScript('test1'); - -//dump($wa->getEsmImportMap()); ?> From 0f4b1c0e7d732310dc252ab7a1fd386de6519bb6 Mon Sep 17 00:00:00 2001 From: Fedir Zinchuk Date: Sun, 4 Jun 2023 13:46:16 +0300 Subject: [PATCH 06/11] Update build/media_source/system/js/esm-map.js Co-authored-by: Brian Teeman --- build/media_source/system/js/esm-map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/media_source/system/js/esm-map.js b/build/media_source/system/js/esm-map.js index 3a47d46f2e7e0..569b59b415256 100644 --- a/build/media_source/system/js/esm-map.js +++ b/build/media_source/system/js/esm-map.js @@ -1,5 +1,5 @@ /** - * @copyright (C) 2018 Open Source Matters, Inc. + * @copyright (C) 2023 Open Source Matters, Inc. * @license GNU General Public License version 2 or later; see LICENSE.txt */ From 6b8eddf94bd866c1660045be8c5fbee553d9e87e Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 5 Jun 2023 13:18:11 +0300 Subject: [PATCH 07/11] EsmImportMap add es-module-shims --- .../init/localise-packages.es6.js | 22 ++++++++++++++- .../init/minify-vendor.es6.js | 1 + build/build-modules-js/settings.json | 18 +++++++++++++ build/media_source/system/joomla.asset.json | 8 ------ build/media_source/system/js/esm-map.js | 27 ------------------- package-lock.json | 11 ++++++++ package.json | 1 + 7 files changed, 52 insertions(+), 36 deletions(-) delete mode 100644 build/media_source/system/js/esm-map.js diff --git a/build/build-modules-js/init/localise-packages.es6.js b/build/build-modules-js/init/localise-packages.es6.js index 8133a6c5c090d..605247a5c0ad6 100644 --- a/build/build-modules-js/init/localise-packages.es6.js +++ b/build/build-modules-js/init/localise-packages.es6.js @@ -4,9 +4,29 @@ const { const { dirname, join } = require('path'); const { codeMirror } = require('./exemptions/codemirror.es6.js'); const { tinyMCE } = require('./exemptions/tinymce.es6.js'); +const fs = require("fs"); const RootPath = process.cwd(); +/** + * Find full path for package file. + * Replacement for require.resolve(), as it is broken for packages with "exports" property. + * + * @param {string} relativePath Relative path to the file to resolve, in format packageName/file-name.js + * @returns {string|boolean} + */ +const resolvePackageFile = (relativePath) => { + for (let i = 0, l = module.paths.length; i < l; i += 1) { + const path = module.paths[i]; + const fullPath = `${path}/${relativePath}`; + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + return false; +}; + /** * * @param {object} files the object of files map, eg {"src.js": "js/src.js"} @@ -39,7 +59,7 @@ const copyFilesTo = async (files, srcDir, destDir) => { */ const resolvePackage = async (vendor, packageName, mediaVendorPath, options, registry) => { const vendorName = vendor.name || packageName; - const modulePathJson = require.resolve(`${packageName}/package.json`); + const modulePathJson = resolvePackageFile(`${packageName}/package.json`); const modulePathRoot = dirname(modulePathJson); // eslint-disable-next-line global-require, import/no-dynamic-require const moduleOptions = require(modulePathJson); diff --git a/build/build-modules-js/init/minify-vendor.es6.js b/build/build-modules-js/init/minify-vendor.es6.js index 1825228b02aab..686b90564775a 100644 --- a/build/build-modules-js/init/minify-vendor.es6.js +++ b/build/build-modules-js/init/minify-vendor.es6.js @@ -11,6 +11,7 @@ const folders = [ 'media/vendor/codemirror', 'media/vendor/debugbar', 'media/vendor/diff/js', + 'media/vendor/es-module-shims/js', 'media/vendor/qrcode/js', 'media/vendor/short-and-sweet/js', 'media/vendor/webcomponentsjs/js', diff --git a/build/build-modules-js/settings.json b/build/build-modules-js/settings.json index b46d54e373f50..147056b923e03 100644 --- a/build/build-modules-js/settings.json +++ b/build/build-modules-js/settings.json @@ -340,6 +340,24 @@ "dependencies": [], "licenseFilename": "license" }, + "es-module-shims": { + "name": "es-module-shims", + "js": { + "dist/es-module-shims.js": "js/es-module-shims.js" + }, + "provideAssets": [ + { + "name": "es-module-shims", + "type": "script", + "uri": "es-module-shims.min.js", + "attributes": { + "async": true + } + } + ], + "dependencies": [], + "licenseFilename": "LICENSE" + }, "focus-visible": { "name": "focus-visible", "js": { diff --git a/build/media_source/system/joomla.asset.json b/build/media_source/system/joomla.asset.json index 21cbfda4f087d..ec9194a0f72d6 100644 --- a/build/media_source/system/joomla.asset.json +++ b/build/media_source/system/joomla.asset.json @@ -36,14 +36,6 @@ "type": "module" } }, - { - "name": "esm-map", - "type": "script", - "uri": "system/esm-map.min.js", - "attributes": { - "type": "module" - } - }, { "name": "messages-legacy", "type": "script", diff --git a/build/media_source/system/js/esm-map.js b/build/media_source/system/js/esm-map.js deleted file mode 100644 index 569b59b415256..0000000000000 --- a/build/media_source/system/js/esm-map.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @copyright (C) 2023 Open Source Matters, Inc. - * @license GNU General Public License version 2 or later; see LICENSE.txt - */ - -let map; -/** - * Resolve module name to it's URL when "importmap" is not supported, - * otherwise return module name unchanged. - * - * Usage example: - * const { foo } = await import(joomlaESMap('@bar-module')); - * or - * import(joomlaESMap('@bar-module')).then(({ foo }) => { ... }); - * - * @param {String} name Module name - * @param {boolean} force Force to resolve, even when "importmap" is supported - * @returns {String} - */ -window.joomlaESMap = (name, force) => { - if (HTMLScriptElement.supports('importmap') && !force) return name; - if (!map) { - const m = document.querySelector('script[type="importmap"]'); - map = m ? JSON.parse(m.textContent) : {imports: {}}; - } - return map.imports[name] || name; -}; diff --git a/package-lock.json b/package-lock.json index 437be0c0e72fa..9129143bbe2d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "diff": "^5.1.0", "dotenv": "^16.0.3", "dragula": "^3.7.3", + "es-module-shims": "^1.7.3", "focus-visible": "^5.2.0", "hotkeys-js": "^3.10.2", "joomla-ui-custom-elements": "^0.2.0", @@ -4241,6 +4242,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-module-shims": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.7.3.tgz", + "integrity": "sha512-RntbjDrPCciiM3MEfnnr43u82/gRZ2ukS7zlUJOPMgorN1xqQ12uxCsvEPWTi/ozaOHBrpxJHd3qvhUQnN6XfQ==" + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -13238,6 +13244,11 @@ "which-typed-array": "^1.1.9" } }, + "es-module-shims": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.7.3.tgz", + "integrity": "sha512-RntbjDrPCciiM3MEfnnr43u82/gRZ2ukS7zlUJOPMgorN1xqQ12uxCsvEPWTi/ozaOHBrpxJHd3qvhUQnN6XfQ==" + }, "es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", diff --git a/package.json b/package.json index 6def42d699d79..ab9afd9905c5e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "diff": "^5.1.0", "dotenv": "^16.0.3", "dragula": "^3.7.3", + "es-module-shims": "^1.7.3", "focus-visible": "^5.2.0", "hotkeys-js": "^3.10.2", "joomla-ui-custom-elements": "^0.2.0", From e073100e486b163c2af4d483a82405fe1e11bb92 Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 5 Jun 2023 14:18:55 +0300 Subject: [PATCH 08/11] EsmImportMap enable es-module-shims --- .../Document/Renderer/Html/MetasRenderer.php | 4 +++ .../Renderer/Html/ScriptsRenderer.php | 36 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/libraries/src/Document/Renderer/Html/MetasRenderer.php b/libraries/src/Document/Renderer/Html/MetasRenderer.php index 6c8b2d2175356..c7e07495ec88b 100644 --- a/libraries/src/Document/Renderer/Html/MetasRenderer.php +++ b/libraries/src/Document/Renderer/Html/MetasRenderer.php @@ -50,6 +50,10 @@ public function render($head, $params = [], $content = null) $app = Factory::getApplication(); $wa = $this->_doc->getWebAssetManager(); + // Explicitly enable "es-module-shims" for importmap polyfill, before the CompileHead event, + // that allows for developers to disable it when they not need the polyfill + $wa->useScript('es-module-shims'); + // Check for AttachBehavior and web components foreach ($wa->getAssets('script', true) as $asset) { if ($asset instanceof WebAssetAttachBehaviorInterface) { diff --git a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php index 3238086b23b78..835ee061c4c13 100644 --- a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php +++ b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php @@ -141,7 +141,7 @@ private function renderElement($item): string $src = $asset ? $asset->getUri() : ($item['src'] ?? ''); // Make sure we have a src, and it not already rendered - if (!$src || !empty($this->renderedSrc[$src]) || ($asset && $asset->getOption('importmapOnly'))) { + if (!$src || !empty($this->renderedSrc[$src])) { return ''; } @@ -333,7 +333,7 @@ private function renderAttributes(array $attributes): string * * @since __DEPLOY_VERSION__ */ - private function renderImportmap(array $assets) + private function renderImportmap(array &$assets) { $buffer = ''; $importmap = ['imports' => []]; @@ -341,7 +341,7 @@ private function renderImportmap(array $assets) $mediaVersion = $this->_doc->getMediaVersion(); // Collect a modules for the map - foreach ($assets as $item) { + foreach ($assets as $k => $item) { // Only importmap:true can be mapped if (!$item->getOption('importmap')) { continue; @@ -352,6 +352,10 @@ private function renderImportmap(array $assets) $version = $item->getVersion(); $src = $item->getUri(); + if (!$src) { + continue; + } + // Check if script uses media version. if ($version && !str_contains($src, '?') && !str_ends_with($src, '/') && ($mediaVersion || $version !== 'auto')) { $src .= '?' . ($version === 'auto' ? $mediaVersion : $version); @@ -362,14 +366,36 @@ private function renderImportmap(array $assets) } else { $importmap['scopes'][$esmScope][$esmName] = $src; } + + // Remove the item from list of assets after it were added to map, for assets with "importmapOnly". + if ($item->getOption('importmapOnly')) { + unset($assets[$k]); + } } - // Render map if (!empty($importmap['imports'])) { + // Add polyfill when exists + if (!empty($assets['es-module-shims'])) { + $buffer .= $this->renderElement($assets['es-module-shims']); + } + + // Render importmap $jsonImports = json_encode($importmap, JDEBUG ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES); - $buffer .= $tab . ''; + $attribs = ['type' => 'importmap']; + + // Add "nonce" attribute if exist + if ($this->_doc->cspNonce && !is_null($this->_doc->cspNonce)) { + $attribs['nonce'] = $this->_doc->cspNonce; + } + + $buffer .= $tab . 'renderAttributes($attribs); + $buffer .= '>' . $jsonImports . ''; } + // Remove polyfill for "importmap" from assets list + unset($assets['es-module-shims']); + return $buffer; } } From f0e523d74a5d97f3ef8dd6a52ac7960507296811 Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 5 Jun 2023 14:23:44 +0300 Subject: [PATCH 09/11] EsmImportMap upd --- build/build-modules-js/init/localise-packages.es6.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build/build-modules-js/init/localise-packages.es6.js b/build/build-modules-js/init/localise-packages.es6.js index 605247a5c0ad6..6f99e50ed4acc 100644 --- a/build/build-modules-js/init/localise-packages.es6.js +++ b/build/build-modules-js/init/localise-packages.es6.js @@ -4,7 +4,6 @@ const { const { dirname, join } = require('path'); const { codeMirror } = require('./exemptions/codemirror.es6.js'); const { tinyMCE } = require('./exemptions/tinymce.es6.js'); -const fs = require("fs"); const RootPath = process.cwd(); @@ -19,7 +18,7 @@ const resolvePackageFile = (relativePath) => { for (let i = 0, l = module.paths.length; i < l; i += 1) { const path = module.paths[i]; const fullPath = `${path}/${relativePath}`; - if (fs.existsSync(fullPath)) { + if (existsSync(fullPath)) { return fullPath; } } From f9f244ae6267a3a9cac20bc315eea0cde45267ad Mon Sep 17 00:00:00 2001 From: Fedik Date: Mon, 5 Jun 2023 17:16:32 +0300 Subject: [PATCH 10/11] EsmImportMap keep es-module-shims disabled by default --- libraries/src/Document/Renderer/Html/MetasRenderer.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/src/Document/Renderer/Html/MetasRenderer.php b/libraries/src/Document/Renderer/Html/MetasRenderer.php index c7e07495ec88b..6c8b2d2175356 100644 --- a/libraries/src/Document/Renderer/Html/MetasRenderer.php +++ b/libraries/src/Document/Renderer/Html/MetasRenderer.php @@ -50,10 +50,6 @@ public function render($head, $params = [], $content = null) $app = Factory::getApplication(); $wa = $this->_doc->getWebAssetManager(); - // Explicitly enable "es-module-shims" for importmap polyfill, before the CompileHead event, - // that allows for developers to disable it when they not need the polyfill - $wa->useScript('es-module-shims'); - // Check for AttachBehavior and web components foreach ($wa->getAssets('script', true) as $asset) { if ($asset instanceof WebAssetAttachBehaviorInterface) { From a1117d83c047e3e6d595ef6a629e725cd9d96007 Mon Sep 17 00:00:00 2001 From: Fedik Date: Thu, 22 Jun 2023 19:46:17 +0300 Subject: [PATCH 11/11] Remove importmapOnly --- libraries/src/Document/Renderer/Html/ScriptsRenderer.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php index 835ee061c4c13..022b8203b1202 100644 --- a/libraries/src/Document/Renderer/Html/ScriptsRenderer.php +++ b/libraries/src/Document/Renderer/Html/ScriptsRenderer.php @@ -367,10 +367,8 @@ private function renderImportmap(array &$assets) $importmap['scopes'][$esmScope][$esmName] = $src; } - // Remove the item from list of assets after it were added to map, for assets with "importmapOnly". - if ($item->getOption('importmapOnly')) { - unset($assets[$k]); - } + // Remove the item from list of assets after it were added to the map. + unset($assets[$k]); } if (!empty($importmap['imports'])) {