From c1f01bec3716ecc3934f7ba4598bd06740b97ced Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Mon, 13 May 2024 14:16:17 -0400 Subject: [PATCH 1/7] Add support for ESM import map feature. --- .../core/Mage/Core/Model/Design/Package.php | 116 ++++++++++++++++++ app/code/core/Mage/Page/Block/Html/Head.php | 77 ++++++++++++ dev/openmage/nginx-admin.conf | 7 ++ dev/openmage/nginx-frontend.conf | 7 ++ media/.htaccess | 6 + 5 files changed, 213 insertions(+) diff --git a/app/code/core/Mage/Core/Model/Design/Package.php b/app/code/core/Mage/Core/Model/Design/Package.php index c10a40af2d8..1e713b7ff93 100644 --- a/app/code/core/Mage/Core/Model/Design/Package.php +++ b/app/code/core/Mage/Core/Model/Design/Package.php @@ -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; } @@ -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'); + 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); + 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 = ''; + } 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 = ''; + } else { + $html = ''; + } + } + 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 diff --git a/app/code/core/Mage/Page/Block/Html/Head.php b/app/code/core/Mage/Page/Block/Html/Head.php index 3a1a4a4816a..475ef6e80a1 100644 --- a/app/code/core/Mage/Page/Block/Html/Head.php +++ b/app/code/core/Mage/Page/Block/Html/Head.php @@ -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; @@ -578,4 +580,79 @@ 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); + } + } diff --git a/dev/openmage/nginx-admin.conf b/dev/openmage/nginx-admin.conf index debd5cbd5ce..1869e6724e9 100644 --- a/dev/openmage/nginx-admin.conf +++ b/dev/openmage/nginx-admin.conf @@ -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"; diff --git a/dev/openmage/nginx-frontend.conf b/dev/openmage/nginx-frontend.conf index ddc1d51873e..aa0d654e9e4 100644 --- a/dev/openmage/nginx-frontend.conf +++ b/dev/openmage/nginx-frontend.conf @@ -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"; diff --git a/media/.htaccess b/media/.htaccess index 2ca3c32b8f8..60a1f51361e 100644 --- a/media/.htaccess +++ b/media/.htaccess @@ -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 + + AddType application/importmap+json .json + + ############################################ From dc9f6fc00ed522a662c1c8961389c57524b8e648 Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Tue, 14 May 2024 11:03:55 -0400 Subject: [PATCH 2/7] Apply suggestions from code review Co-authored-by: Fabrizio Balliano --- app/code/core/Mage/Core/Model/Design/Package.php | 6 +++--- app/code/core/Mage/Page/Block/Html/Head.php | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/code/core/Mage/Core/Model/Design/Package.php b/app/code/core/Mage/Core/Model/Design/Package.php index 1e713b7ff93..560d4539d00 100644 --- a/app/code/core/Mage/Core/Model/Design/Package.php +++ b/app/code/core/Mage/Core/Model/Design/Package.php @@ -964,7 +964,7 @@ public function renderImportMap(array $allItems, array $itemUrls, bool $inline): if ($useCache) { $cacheKey[] = Mage::getIsDeveloperMode() ? $filePath . '-' . filemtime($filePath) : $filePath; } - $importData = json_decode($filePath, TRUE, 3, JSON_THROW_ON_ERROR); + $importData = json_decode($filePath, true, 3, JSON_THROW_ON_ERROR); if (isset($importData['imports'])) { $importMap['imports'] = array_merge($importMap['imports'] ?? [], $importData['imports']); } @@ -986,7 +986,7 @@ public function renderImportMap(array $allItems, array $itemUrls, bool $inline): break; } } - if ( ! $importMap) { + if (!$importMap) { return ''; } @@ -1000,7 +1000,7 @@ public function renderImportMap(array $allItems, array $itemUrls, bool $inline): // Allow devs to bypass cache in browser if ($useCache && Mage::getIsDeveloperMode() && isset($_SERVER['HTTP_CACHE_CONTROL']) - && strpos($_SERVER['HTTP_CACHE_CONTROL'], 'no-cache') !== FALSE + && strpos($_SERVER['HTTP_CACHE_CONTROL'], 'no-cache') !== false ) { $html = null; } diff --git a/app/code/core/Mage/Page/Block/Html/Head.php b/app/code/core/Mage/Page/Block/Html/Head.php index 475ef6e80a1..8132f316855 100644 --- a/app/code/core/Mage/Page/Block/Html/Head.php +++ b/app/code/core/Mage/Page/Block/Html/Head.php @@ -654,5 +654,4 @@ public function _prepareImportMap(bool $inline): string { return Mage::getSingleton('core/design_package')->renderImportMap($this->_data['items'], $this->_data['imports'] ?? [], $inline); } - } From fe7a3b32b20079deb58da971f82efd983f00fd34 Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Tue, 14 May 2024 11:13:36 -0400 Subject: [PATCH 3/7] Update app/code/core/Mage/Core/Model/Design/Package.php --- app/code/core/Mage/Core/Model/Design/Package.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/core/Mage/Core/Model/Design/Package.php b/app/code/core/Mage/Core/Model/Design/Package.php index 560d4539d00..5efa4c41b63 100644 --- a/app/code/core/Mage/Core/Model/Design/Package.php +++ b/app/code/core/Mage/Core/Model/Design/Package.php @@ -994,7 +994,7 @@ public function renderImportMap(array $allItems, array $itemUrls, bool $inline): if ($useCache) { $cacheKey = md5('LAYOUT_' . $this->getArea() . '_STORE' . $this->getStore()->getId() . '_' . $this->getPackageName() . '_' . $this->getTheme('layout') . '_' - . Mage::app()->getRequest()->isSecure() ? 's' : 'u' . '_' . implode('|', $cacheKey)); + . (Mage::app()->getRequest()->isSecure() ? 's' : 'u') . '_' . implode('|', $cacheKey)); $html = Mage::app()->loadCache($cacheKey); } From 0e4b8f6eca10b662fb94a8d6b34f5959feffe8b4 Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Tue, 14 May 2024 11:13:41 -0400 Subject: [PATCH 4/7] Update app/code/core/Mage/Page/Block/Html/Head.php --- app/code/core/Mage/Page/Block/Html/Head.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/core/Mage/Page/Block/Html/Head.php b/app/code/core/Mage/Page/Block/Html/Head.php index 8132f316855..9abf0b718e6 100644 --- a/app/code/core/Mage/Page/Block/Html/Head.php +++ b/app/code/core/Mage/Page/Block/Html/Head.php @@ -208,7 +208,7 @@ public function getCssJsHtml() $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"; + $html .= $this->_prepareImportMap(!$externalImportMap) . "\n"; foreach ($lines as $if => $items) { if (empty($items)) { continue; From 19fd02460d3ee6f626a5163ab013f0d03fe1dce9 Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Tue, 14 May 2024 11:37:09 -0400 Subject: [PATCH 5/7] Update app/code/core/Mage/Core/Model/Design/Package.php --- app/code/core/Mage/Core/Model/Design/Package.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/core/Mage/Core/Model/Design/Package.php b/app/code/core/Mage/Core/Model/Design/Package.php index 5efa4c41b63..43c60f91931 100644 --- a/app/code/core/Mage/Core/Model/Design/Package.php +++ b/app/code/core/Mage/Core/Model/Design/Package.php @@ -961,10 +961,13 @@ public function renderImportMap(array $allItems, array $itemUrls, bool $inline): $filePath = Mage::getBaseDir() . DS . 'js' . DS . $fileOrUrl; } $fileHashKey [] = $item['name']; + if (!($fileData = file_get_contents($filePath)) { + throw new Exception('Could not read importmap file or file is empty: ' . $filePath); + } if ($useCache) { $cacheKey[] = Mage::getIsDeveloperMode() ? $filePath . '-' . filemtime($filePath) : $filePath; } - $importData = json_decode($filePath, true, 3, JSON_THROW_ON_ERROR); + $importData = json_decode($fileData, true, 3, JSON_THROW_ON_ERROR); if (isset($importData['imports'])) { $importMap['imports'] = array_merge($importMap['imports'] ?? [], $importData['imports']); } From ac374ba46960cf077bbc355843b766f232a9d85d Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Tue, 14 May 2024 11:48:26 -0400 Subject: [PATCH 6/7] Update app/code/core/Mage/Core/Model/Design/Package.php --- app/code/core/Mage/Core/Model/Design/Package.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/core/Mage/Core/Model/Design/Package.php b/app/code/core/Mage/Core/Model/Design/Package.php index 43c60f91931..cf8d15a2c02 100644 --- a/app/code/core/Mage/Core/Model/Design/Package.php +++ b/app/code/core/Mage/Core/Model/Design/Package.php @@ -946,7 +946,7 @@ public function renderImportMap(array $allItems, array $itemUrls, bool $inline): $importMap = []; $cacheKey = []; $fileHashKey = []; // No timestamps in hash key in production mode - $useCache = Mage::app()->useCache('import_map'); + $useCache = !$inline && Mage::app()->useCache('import_map'); foreach ($allItems as $item) { if (!preg_match('#^(static|skin)_import(_map)?$#', $item['type']) || !isset($itemUrls[$item['type']][$item['name']])) { continue; From 73e74e642d649cce1c89361e90f73c109502823f Mon Sep 17 00:00:00 2001 From: Colin Mollenhour Date: Tue, 14 May 2024 11:49:51 -0400 Subject: [PATCH 7/7] Update Package.php --- app/code/core/Mage/Core/Model/Design/Package.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/core/Mage/Core/Model/Design/Package.php b/app/code/core/Mage/Core/Model/Design/Package.php index cf8d15a2c02..b65703c8eee 100644 --- a/app/code/core/Mage/Core/Model/Design/Package.php +++ b/app/code/core/Mage/Core/Model/Design/Package.php @@ -961,7 +961,7 @@ public function renderImportMap(array $allItems, array $itemUrls, bool $inline): $filePath = Mage::getBaseDir() . DS . 'js' . DS . $fileOrUrl; } $fileHashKey [] = $item['name']; - if (!($fileData = file_get_contents($filePath)) { + if (!($fileData = file_get_contents($filePath))) { throw new Exception('Could not read importmap file or file is empty: ' . $filePath); } if ($useCache) {