Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.0] ESM importmap support #40714

Merged
merged 13 commits into from
Jun 24, 2023
8 changes: 8 additions & 0 deletions build/media_source/system/joomla.asset.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 27 additions & 0 deletions build/media_source/system/js/esm-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org>
Fedik marked this conversation as resolved.
Show resolved Hide resolved
* @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}
Fedik marked this conversation as resolved.
Show resolved Hide resolved
*/
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;
};
54 changes: 53 additions & 1 deletion libraries/src/Document/Renderer/Html/ScriptsRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public function render($head, $params = [], $content = null)
$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);

Expand Down Expand Up @@ -138,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 '';
}

Expand Down Expand Up @@ -320,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 importmap:true can be mapped
if (!$item->getOption('importmap')) {
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 . '<script type="importmap">' . $jsonImports . '</script>';
}

return $buffer;
}
}
5 changes: 5 additions & 0 deletions libraries/src/WebAsset/WebAssetItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down