Skip to content

Commit de1a338

Browse files
Fedikbrianteeman
andauthored
[5.0] ESM importmap support (#40714)
* EsmImportMap add es-module-shims * EsmImportMap enable es-module-shims * EsmImportMap keep es-module-shims disabled by default * Remove importmapOnly --------- Co-authored-by: Brian Teeman <[email protected]>
1 parent e6518fb commit de1a338

File tree

7 files changed

+133
-2
lines changed

7 files changed

+133
-2
lines changed

build/build-modules-js/init/localise-packages.es6.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ const { tinyMCE } = require('./exemptions/tinymce.es6.js');
77

88
const RootPath = process.cwd();
99

10+
/**
11+
* Find full path for package file.
12+
* Replacement for require.resolve(), as it is broken for packages with "exports" property.
13+
*
14+
* @param {string} relativePath Relative path to the file to resolve, in format packageName/file-name.js
15+
* @returns {string|boolean}
16+
*/
17+
const resolvePackageFile = (relativePath) => {
18+
for (let i = 0, l = module.paths.length; i < l; i += 1) {
19+
const path = module.paths[i];
20+
const fullPath = `${path}/${relativePath}`;
21+
if (existsSync(fullPath)) {
22+
return fullPath;
23+
}
24+
}
25+
26+
return false;
27+
};
28+
1029
/**
1130
*
1231
* @param {object} files the object of files map, eg {"src.js": "js/src.js"}
@@ -39,7 +58,7 @@ const copyFilesTo = async (files, srcDir, destDir) => {
3958
*/
4059
const resolvePackage = async (vendor, packageName, mediaVendorPath, options, registry) => {
4160
const vendorName = vendor.name || packageName;
42-
const modulePathJson = require.resolve(`${packageName}/package.json`);
61+
const modulePathJson = resolvePackageFile(`${packageName}/package.json`);
4362
const modulePathRoot = dirname(modulePathJson);
4463
// eslint-disable-next-line global-require, import/no-dynamic-require
4564
const moduleOptions = require(modulePathJson);

build/build-modules-js/init/minify-vendor.es6.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const folders = [
1111
'media/vendor/codemirror',
1212
'media/vendor/debugbar',
1313
'media/vendor/diff/js',
14+
'media/vendor/es-module-shims/js',
1415
'media/vendor/qrcode/js',
1516
'media/vendor/short-and-sweet/js',
1617
'media/vendor/webcomponentsjs/js',

build/build-modules-js/settings.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,24 @@
341341
"dependencies": [],
342342
"licenseFilename": "license"
343343
},
344+
"es-module-shims": {
345+
"name": "es-module-shims",
346+
"js": {
347+
"dist/es-module-shims.js": "js/es-module-shims.js"
348+
},
349+
"provideAssets": [
350+
{
351+
"name": "es-module-shims",
352+
"type": "script",
353+
"uri": "es-module-shims.min.js",
354+
"attributes": {
355+
"async": true
356+
}
357+
}
358+
],
359+
"dependencies": [],
360+
"licenseFilename": "LICENSE"
361+
},
344362
"focus-visible": {
345363
"name": "focus-visible",
346364
"js": {

libraries/src/Document/Renderer/Html/ScriptsRenderer.php

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public function render($head, $params = [], $content = null)
5656
$inlineAssets = $wam->filterOutInlineAssets($assets);
5757
$inlineRelation = $wam->getInlineRelation($inlineAssets);
5858

59+
// Generate importmap first
60+
$buffer .= $this->renderImportmap($assets);
61+
5962
// Merge with existing scripts, for rendering
6063
$assets = array_merge(array_values($assets), $this->_doc->_scripts);
6164

@@ -138,7 +141,7 @@ private function renderElement($item): string
138141
$src = $asset ? $asset->getUri() : ($item['src'] ?? '');
139142

140143
// Make sure we have a src, and it not already rendered
141-
if (!$src || !empty($this->renderedSrc[$src]) || ($asset && $asset->getOption('webcomponent'))) {
144+
if (!$src || !empty($this->renderedSrc[$src])) {
142145
return '';
143146
}
144147

@@ -320,4 +323,77 @@ private function renderAttributes(array $attributes): string
320323

321324
return $buffer;
322325
}
326+
327+
/**
328+
* Renders ESM importmap element
329+
*
330+
* @param WebAssetItemInterface[] $assets The assets list
331+
*
332+
* @return string The attributes string
333+
*
334+
* @since __DEPLOY_VERSION__
335+
*/
336+
private function renderImportmap(array &$assets)
337+
{
338+
$buffer = '';
339+
$importmap = ['imports' => []];
340+
$tab = $this->_doc->_getTab();
341+
$mediaVersion = $this->_doc->getMediaVersion();
342+
343+
// Collect a modules for the map
344+
foreach ($assets as $k => $item) {
345+
// Only importmap:true can be mapped
346+
if (!$item->getOption('importmap')) {
347+
continue;
348+
}
349+
350+
$esmName = $item->getOption('importmapName') ?: $item->getName();
351+
$esmScope = $item->getOption('importmapScope');
352+
$version = $item->getVersion();
353+
$src = $item->getUri();
354+
355+
if (!$src) {
356+
continue;
357+
}
358+
359+
// Check if script uses media version.
360+
if ($version && !str_contains($src, '?') && !str_ends_with($src, '/') && ($mediaVersion || $version !== 'auto')) {
361+
$src .= '?' . ($version === 'auto' ? $mediaVersion : $version);
362+
}
363+
364+
if (!$esmScope) {
365+
$importmap['imports'][$esmName] = $src;
366+
} else {
367+
$importmap['scopes'][$esmScope][$esmName] = $src;
368+
}
369+
370+
// Remove the item from list of assets after it were added to the map.
371+
unset($assets[$k]);
372+
}
373+
374+
if (!empty($importmap['imports'])) {
375+
// Add polyfill when exists
376+
if (!empty($assets['es-module-shims'])) {
377+
$buffer .= $this->renderElement($assets['es-module-shims']);
378+
}
379+
380+
// Render importmap
381+
$jsonImports = json_encode($importmap, JDEBUG ? JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES : JSON_UNESCAPED_SLASHES);
382+
$attribs = ['type' => 'importmap'];
383+
384+
// Add "nonce" attribute if exist
385+
if ($this->_doc->cspNonce && !is_null($this->_doc->cspNonce)) {
386+
$attribs['nonce'] = $this->_doc->cspNonce;
387+
}
388+
389+
$buffer .= $tab . '<script';
390+
$buffer .= $this->renderAttributes($attribs);
391+
$buffer .= '>' . $jsonImports . '</script>';
392+
}
393+
394+
// Remove polyfill for "importmap" from assets list
395+
unset($assets['es-module-shims']);
396+
397+
return $buffer;
398+
}
323399
}

libraries/src/WebAsset/WebAssetItem.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
namespace Joomla\CMS\WebAsset;
1111

1212
use Joomla\CMS\HTML\HTMLHelper;
13+
use Joomla\CMS\Uri\Uri;
1314

1415
// phpcs:disable PSR1.Files.SideEffects
1516
\defined('_JEXEC') or die;
@@ -175,6 +176,10 @@ public function getUri($resolvePath = true): string
175176
$path = $this->resolvePath($path, 'stylesheet');
176177
break;
177178
default:
179+
// Asset for the ES modules may give us a folder for ESM import map
180+
if (str_ends_with($path, '/') && !str_starts_with($path, '.')) {
181+
$path = Uri::root(true) . '/' . $path;
182+
}
178183
break;
179184
}
180185
}

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"diff": "^5.1.0",
5353
"dotenv": "^16.0.3",
5454
"dragula": "^3.7.3",
55+
"es-module-shims": "^1.7.3",
5556
"focus-visible": "^5.2.0",
5657
"hotkeys-js": "^3.10.2",
5758
"joomla-ui-custom-elements": "^0.2.0",

0 commit comments

Comments
 (0)