From b1d321445aeca3e4c59100630571409c75c02442 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Thu, 11 May 2023 19:02:32 -0700 Subject: [PATCH] (REF) Split ImportMap in two (ImportMap, BrowserLoader) --- CRM/Core/Region.php | 4 +- CRM/Core/Resources/CollectionTrait.php | 4 +- Civi/Core/Container.php | 2 + Civi/Esm/BrowserLoader.php | 165 +++++++++++++++++++++++++ Civi/Esm/ImportMap.php | 140 +-------------------- 5 files changed, 177 insertions(+), 138 deletions(-) create mode 100644 Civi/Esm/BrowserLoader.php diff --git a/CRM/Core/Region.php b/CRM/Core/Region.php index b18cce15db..7cb7f22254 100644 --- a/CRM/Core/Region.php +++ b/CRM/Core/Region.php @@ -90,7 +90,7 @@ class CRM_Core_Region implements CRM_Core_Resources_CollectionInterface, CRM_Cor case 'scriptUrl': // ECMAScript Modules (ESMs) are basically Javascript files, but they require a slightly different incantation. if (!empty($snippet['esm'])) { - $html .= sprintf("\n", $snippet['scriptUrl']); + $html .= Civi::service('esm.loader')->renderModule($snippet); } elseif (!$allowCmsOverride || !$cms->addScriptUrl($snippet['scriptUrl'], $this->_name)) { $html .= sprintf("\n", $snippet['scriptUrl']); @@ -113,7 +113,7 @@ class CRM_Core_Region implements CRM_Core_Resources_CollectionInterface, CRM_Cor case 'script': // ECMAScript Modules (ESMs) are basically Javascript files, but they require a slightly different incantation. if (!empty($snippet['esm'])) { - $html .= sprintf("\n", $snippet['script']); + $html .= Civi::service('esm.loader')->renderModule($snippet); } elseif (!$allowCmsOverride || !$cms->addScript($snippet['script'], $this->_name)) { $html .= sprintf("\n", $snippet['script']); diff --git a/CRM/Core/Resources/CollectionTrait.php b/CRM/Core/Resources/CollectionTrait.php index 63927faf51..15daa1813f 100644 --- a/CRM/Core/Resources/CollectionTrait.php +++ b/CRM/Core/Resources/CollectionTrait.php @@ -9,6 +9,8 @@ +--------------------------------------------------------------------+ */ +use Civi\Core\Event\GenericHookEvent; + /** * Class CRM_Core_Resources_CollectionTrait * @@ -102,7 +104,7 @@ trait CRM_Core_Resources_CollectionTrait { } } if (!empty($snippet['esm'])) { - Civi::service('esm.import_map')->useModule($snippet); + Civi::dispatcher()->dispatch('civi.esm.useModule', GenericHookEvent::create(['snippet' => &$snippet])); } if ($snippet['type'] === 'scriptFile' && !isset($snippet['scriptFileUrls'])) { diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index 5b983fb637..b7907b99db 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -392,6 +392,8 @@ class Container { [new Reference('action_object_provider')] ); + $container->setAlias('esm.loader', 'esm.loader.browser')->setPublic(TRUE); + \CRM_Utils_Hook::container($container); return $container; diff --git a/Civi/Esm/BrowserLoader.php b/Civi/Esm/BrowserLoader.php new file mode 100644 index 0000000000..4964b1e285 --- /dev/null +++ b/Civi/Esm/BrowserLoader.php @@ -0,0 +1,165 @@ + + * { "import": {"civicrm/": "https://example.com/sites/all/modules/civicrm"}} + * + * + * + * This should be the simplest and most efficient way to load modules. However, there may be + * compatibility issues with older browsers or future UFs. + * + * @service esm.loader.browser + */ +class BrowserLoader extends \Civi\Core\Service\AutoService implements EventSubscriberInterface { + + /** + * @return array + */ + public static function getSubscribedEvents() { + return [ + '&civi.esm.useModule' => 'onUseModule', + '&civi.region.render' => 'onRegionRender', + ]; + } + + /** + * Should we generate tags like `' + */ + protected function renderImportMap(array $importMap): string { + if (!$this->enableMap || empty($importMap)) { + return ''; + } + + $flags = JSON_UNESCAPED_SLASHES; + if (Civi::settings()->get('debug_enabled')) { + $flags |= JSON_PRETTY_PRINT; + } + return sprintf("", json_encode($importMap, $flags)); + } + + /** + * @param array $snippet + * The module resource being rendered, as per "CollectionInterface::add()". + * Ex: ['type' => 'scriptUrl', 'scriptUrl' => 'https://example.com/foo.js', 'esm' => TRUE] + * @return string + * HTML + * @see \CRM_Core_Resources_CollectionInterface::add() + */ + public function renderModule(array $snippet): string { + if (!$this->enableModules) { + return ''; + } + + switch ($snippet['type']) { + case 'script': + return sprintf("\n", $snippet['script']); + + case 'scriptUrl': + return sprintf("\n", $snippet['scriptUrl']); + + default: + $class = get_class($this); + Civi::log()->warning($class . ' does not support {type}', ['type' => $snippet['type']]); + return ''; + } + } + +} diff --git a/Civi/Esm/ImportMap.php b/Civi/Esm/ImportMap.php index 118efff700..707b4644dd 100644 --- a/Civi/Esm/ImportMap.php +++ b/Civi/Esm/ImportMap.php @@ -2,15 +2,12 @@ namespace Civi\Esm; -use Civi\Core\Event\GenericHookEvent; -use Civi\Core\HookInterface; - /** * ECMAScript Modules (ESMs) allow you to load a JS file based on a physical-path or a * logical-path. Compare: * - * import { TableWidget } from 'https://example.com/sites/all/modules/civicrm/js/table-widget.js'; - * import { TableWidget } from 'civicrm/js/tab-widget.js'; + * - import { TableWidget } from 'https://example.com/sites/all/modules/civicrm/js/table-widget.js'; + * - import { TableWidget } from 'civicrm/js/tab-widget.js'; * * The logical-path (`civicrm/js/tab-widget.js`) is much easier to read, and it adapts * better to more environments. @@ -21,12 +18,6 @@ use Civi\Core\HookInterface; * { "import": {"civicrm/": "https://example.com/sites/all/modules/civicrm"}} * * - * This service defines the import-map for CiviCRM and its extensions. There are a few - * perspectives on how to use this service. - * - * ################################################################################### - * ## Extension Developer: How to register additional mappings - * * If you are writing Javascript code for an extension, then you may want to define new * mappings, e.g. * @@ -35,114 +26,13 @@ use Civi\Core\HookInterface; * $importMap['imports']['bar/'] = E::url('packages/bar/dist/'); * } * - * ################################################################################### - * ## Core Developer: How to render a default SCRIPT tag - * - * CiviCRM must generate the SCRIPT tag because (at time of writing) none of the supported - * UF's have a mechanism to do so. - * - * - Logic: IF the current page has any ESM modules, THEN display a SCRIPT in the HEAD. - * - * - Implementation: The import-map listens to the `civi.region.render[html-header]` event. - * Whenever the header is generated, it makes a dynamic decision about whether to display. - * - * ################################################################################### - * ## UF/CMS Developer: How to integrate import-maps from Civi and UF/CMS. + * Note: These mappings are used by an ESM Loader, such as "BasicLoader". * - * In the future, UFs may define their own protocols for generating their own import-maps. - * But the browser can only load one import-map. Therefore, the CiviCRM and UF import-maps - * will need to be integrated. Here is how: - * - * 1. Disable CiviCRM's default SCRIPT renderer: - * - * Civi::dispatcher()->addListener('hook_config', fn($e) => Civi::service('esm.import_map')->setAutoInject(FALSE)); - * - * (*You might do this in `CRM_Utils_System_*::initialize()`*) - * - * 2. Get the import-map from CiviCRM: - * - * $importMap = Civi::service('esm.import_map')->get(); - * - * 3. Pass the $importMap to the UF's native API (with any required transformations). + * @see \Civi\Esm\BrowserLoader * * @service esm.import_map */ -class ImportMap extends \Civi\Core\Service\AutoService implements HookInterface { - - /** - * Do we need to send an import-map for the current page-view? - * - * For the moment, we figure this dynamically -- based on whether any "esm" scripts have - * been added. During the early stages (where ESMs aren't in widespread use), this seems - * safer. However, in the future, we might find some kind of race (e.g. where the system - * renders "" before it decides on a specific "' - */ - public function render(array $importMap): string { - if (empty($importMap)) { - return ''; - } - - $flags = JSON_UNESCAPED_SLASHES; - if (\Civi::settings()->get('debug_enabled')) { - $flags |= JSON_PRETTY_PRINT; - } - return sprintf("", json_encode($importMap, $flags)); - } - } -- 2.25.1