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

Feature: ESM Import Maps #3990

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
116 changes: 116 additions & 0 deletions app/code/core/Mage/Core/Model/Design/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ public function cleanMergedJsCss()
{
$result = (bool)$this->_initMergerDir('js', true);
$result = (bool)$this->_initMergerDir('css', true) && $result;
$result = (bool)$this->_initMergerDir('importmap', true) && $result;
return (bool)$this->_initMergerDir('css_secure', true) && $result;
}

Expand Down Expand Up @@ -933,6 +934,121 @@ protected function _prepareUrl($uri)
return $uri;
}

/**
* @param array $allItems
* @param array $itemUrls
* @param bool $inline
* @return string
* @throws JsonException
*/
public function renderImportMap(array $allItems, array $itemUrls, bool $inline): string
{
$importMap = [];
$cacheKey = [];
$fileHashKey = []; // No timestamps in hash key in production mode
$useCache = Mage::app()->useCache('import_map');
colinmollenhour marked this conversation as resolved.
Show resolved Hide resolved
foreach ($allItems as $item) {
if (!preg_match('#^(static|skin)_import(_map)?$#', $item['type']) || !isset($itemUrls[$item['type']][$item['name']])) {
continue;
}
$fileOrUrl = $itemUrls[$item['type']][$item['name']];
switch ($item['type']) {
case 'static_import_map':
case 'skin_import_map':
if ($item['type'] === 'skin_import_map') {
$filePath = $this->getFilename($fileOrUrl, ['_type' => 'skin']);
} else {
$filePath = Mage::getBaseDir() . DS . 'js' . DS . $fileOrUrl;
}
$fileHashKey [] = $item['name'];
if ($useCache) {
$cacheKey[] = Mage::getIsDeveloperMode() ? $filePath . '-' . filemtime($filePath) : $filePath;
}
$importData = json_decode($filePath, true, 3, JSON_THROW_ON_ERROR);
colinmollenhour marked this conversation as resolved.
Show resolved Hide resolved
if (isset($importData['imports'])) {
$importMap['imports'] = array_merge($importMap['imports'] ?? [], $importData['imports']);
}
if (isset($importData['scopes'])) {
$importMap['scopes'] = array_merge($importMap['scopes'] ?? [], $importData['scopes']);
}
break;
case 'static_import':
case 'skin_import':
$fileHashKey [] = $fileOrUrl;
if (!preg_match('#^https?://#', $fileOrUrl)) {
if ($item['type'] === 'skin_import') {
$fileOrUrl = $this->getSkinUrl($fileOrUrl) . '?v=' . filemtime($this->getFilename($fileOrUrl, ['_type' => 'skin']));
} else {
$fileOrUrl = Mage::getBaseUrl('js') . $fileOrUrl . '?v=' . filemtime(Mage::getBaseDir() . DS . 'js' . DS . $fileOrUrl);
}
}
$importMap['imports'][$item['name']] = $fileOrUrl;
break;
}
}
if (!$importMap) {
return '';
}

$html = '';
if ($useCache) {
$cacheKey = md5('LAYOUT_' . $this->getArea() . '_STORE' . $this->getStore()->getId() . '_'
. $this->getPackageName() . '_' . $this->getTheme('layout') . '_'
. (Mage::app()->getRequest()->isSecure() ? 's' : 'u') . '_' . implode('|', $cacheKey));
$html = Mage::app()->loadCache($cacheKey);
}

// Allow devs to bypass cache in browser
if ($useCache && Mage::getIsDeveloperMode() && isset($_SERVER['HTTP_CACHE_CONTROL'])
&& strpos($_SERVER['HTTP_CACHE_CONTROL'], 'no-cache') !== false
) {
$html = null;
}

if (!$html) {
if ($inline) {
$html = '<script type="importmap">' . json_encode($importMap) . '</script>';
} else {
$filePrefix = md5(implode('|', $fileHashKey));
$targetFilename = $filePrefix . '.json';
if ($this->_writeImportMap(json_encode($importMap), $targetFilename)) {
$url = Mage::getBaseUrl('media', Mage::app()->getRequest()->isSecure()) . 'importmap/' . $targetFilename . '?v=' . time();
$html = '<script type="importmap" src="' . $url . '"></script>';
} else {
$html = '<script type="importmap">' . json_encode($importMap) . '</script>';
}
}
if ($useCache) {
// Cache with infinite lifetime since we have filemtime in cache key in dev mode
Mage::app()->saveCache($html, $cacheKey, ['import_map'], null);
}
}
return $html;
}

protected function _writeImportMap(string $data, string $targetFile): bool
{
try {
$targetDir = Mage::getBaseDir('media') . DS . 'importmap';
if (!is_dir($targetDir)) {
if (!mkdir($targetDir)) {
throw new Exception(sprintf('Could not create directory %s.', $targetDir));
}
}
if (!is_writable($targetDir)) {
throw new Exception(sprintf('Path %s is not writeable.', $targetDir));
}
file_put_contents($targetDir . DS . $targetFile, $data, LOCK_EX);
if (Mage::helper('core/file_storage_database')->checkDbUsage()) {
Mage::helper('core/file_storage_database')->saveFile($targetDir . DS . $targetFile);
}
return true;
} catch (Exception $e) {
Mage::logException($e);
return false;
}
}

/**
* Default theme getter
* @return string
Expand Down
76 changes: 76 additions & 0 deletions app/code/core/Mage/Page/Block/Html/Head.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ public function getCssJsHtml()
// prepare HTML
$shouldMergeJs = Mage::getStoreConfigFlag('dev/js/merge_files');
$shouldMergeCss = Mage::getStoreConfigFlag('dev/css/merge_css_files');
$externalImportMap = Mage::getStoreConfigFlag('dev/import_map/external'); // "External import maps are not yet supported."
$html = '';
$html .= $this->_prepareImportMap(!$externalImportMap) . "\n";
foreach ($lines as $if => $items) {
if (empty($items)) {
continue;
Expand Down Expand Up @@ -578,4 +580,78 @@ protected function _sortItems($referenceName, $before, $type)
$this->_data['items'] = $newItems;
}
}

/***************
* Import Maps *
***************/

/**
* @param string $name A unique name for the import map found in a static path (e.g. "main")
* @param string $path The path to the import map file to be merged (relative to the /js/ directory - e.g. bundle/importmap.json)
* @param string|null $devPath An alternative file to use in developer mode
* @param string $referenceName The name of the item to insert the element before. If name is not found, insert at the end, * has special meaning (all)
* @param bool $before If true insert before the $referenceName instead of after
* @return $this
*/
public function addStaticImportMap($name, $path, $devPath = null, $referenceName = '*', $before = false)
{
$this->_data['imports']['static_import_map'][$name] = Mage::getIsDeveloperMode() && $devPath ? $devPath : $path;
$this->addItem('static_import_map', $name, null, null, null, $referenceName, $before);
return $this;
}

/**
* @param string $name A unique name for the import map found in a skin path (e.g. "main")
* @param string $path The path to the import map file to be merged (relative to the skin path - e.g. js/importmap.json)
* @param string|null $devPath An alternative file to use in developer mode
* @param string $referenceName The name of the item to insert the element before. If name is not found, insert at the end, * has special meaning (all)
* @param bool $before If true insert before the $referenceName instead of after
* @return $this
*/
public function addSkinImportMap($name, $path, $devPath = null, $referenceName = '*', $before = false)
{
$this->_data['imports']['skin_import_map'][$name] = Mage::getIsDeveloperMode() && $devPath ? $devPath : $path;
$this->addItem('skin_import_map', $name, null, null, null, $referenceName, $before);
return $this;
}

/**
* @param string $specifier The specifier to use when importing the resource (e.g. "vue")
* @param string $fileOrUrl The path to the file (relative to the /js/ directory) or a CDN url
* @param string|null $devFileOrUrl An alternative file or url to use in developer mode
* @param string $referenceName The name of the item to insert the element before. If name is not found, insert at the end, * has special meaning (all)
* @param bool $before If true insert before the $referenceName instead of after
* @return $this
*/
public function addStaticImport($specifier, $fileOrUrl, $devFileOrUrl = null, $referenceName = '*', $before = false)
{
$this->_data['imports']['static_import'][$specifier] = Mage::getIsDeveloperMode() && $devFileOrUrl ? $devFileOrUrl : $fileOrUrl;
$this->addItem('static_import', $specifier, null, null, null, $referenceName, $before);
return $this;
}

/**
* @param string $specifier The specifier to use when importing the resource (e.g. "vue")
* @param string $fileOrUrl The path to the file (relative to skin) or a CDN url
* @param string|null $devFileOrUrl An alternative file or url to use in developer mode
* @param string $referenceName The name of the item to insert the element before. If name is not found, insert at the end, * has special meaning (all)
* @param bool $before If true insert before the $referenceName instead of after
* @return $this
*/
public function addSkinImport($specifier, $fileOrUrl, $devFileOrUrl = null, $referenceName = '*', $before = false)
{
$this->_data['imports']['skin_import'][$specifier] = Mage::getIsDeveloperMode() && $devFileOrUrl ? $devFileOrUrl : $fileOrUrl;
$this->addItem('skin_import', $specifier, null, null, null, $referenceName, $before);
return $this;
}

/**
* @param bool $inline
* @return string
* @throws JsonException
*/
public function _prepareImportMap(bool $inline): string
{
return Mage::getSingleton('core/design_package')->renderImportMap($this->_data['items'], $this->_data['imports'] ?? [], $inline);
}
}
7 changes: 7 additions & 0 deletions dev/openmage/nginx-admin.conf
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ server {
limit_req zone=media burst=50 nodelay;
root /var/www/html;
gzip on;
location ~* importmap/.*\.json$ {
add_header Cache-Control "public";
expires +1y;
types {
application/importmap+json json;
}
}
location ~* \.(eot|ttf|otf|woff|woff2|svg)$ {
add_header Access-Control-Allow-Origin "*";
add_header Cache-Control "public";
Expand Down
7 changes: 7 additions & 0 deletions dev/openmage/nginx-frontend.conf
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ server {
limit_req zone=media burst=100 nodelay;
root /var/www/html;
gzip on;
location ~* importmap/.*\.json$ {
add_header Cache-Control "public";
expires +1y;
types {
application/importmap+json json;
}
}
location ~* \.(eot|ttf|otf|woff|woff2|svg)$ {
add_header Access-Control-Allow-Origin "*";
add_header Cache-Control "public";
Expand Down
6 changes: 6 additions & 0 deletions media/.htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ Options All -Indexes
AddHandler cgi-script .php .pl .py .jsp .asp .sh .cgi
Options -ExecCGI

############################################
## Serve import maps with correct content type
<FilesMatch "importmap/.*\.json$">
AddType application/importmap+json .json
</FilesMatch>

<IfModule mod_rewrite.c>

############################################
Expand Down
Loading