From ddbced067b16e9fa6fd2cc9d1d6e40a6cfe6d12b Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 16 May 2023 00:24:33 -0700 Subject: [PATCH] Civi/Esm/README.md --- Civi/Esm/README.md | 184 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 Civi/Esm/README.md diff --git a/Civi/Esm/README.md b/Civi/Esm/README.md new file mode 100644 index 0000000000..9b8a6716de --- /dev/null +++ b/Civi/Esm/README.md @@ -0,0 +1,184 @@ +# CiviCRM and ECMAScript Module Loading + +(*This page serves as draft documentation for functionality introduced circa v5.63. It can be migrated to devdocs later.*) + +ECMAScript Modules (ESMs) allow you to load a JS file based on a physical-path or a logical-path. Compare: + +```js +import { TableWidget } from 'https://example.com/sites/all/modules/civicrm/js/table-widget.js'; +import { TableWidget } from 'civicrm/js/tab-widget.js'; +``` + +CiviCRM source-trees may have a variety of file-structures, based on the hosting environment and local configuration. +Consequently, writing valid `import` statements for Civi-related code is much easier with logical-paths. They are +easier to read and easier to adapt. + +Logical-paths must be defined with an import-map. For native browser-based imports, it looks like: + +```html + +``` + +The `importmap` data-structure is described further at: + +* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap +* https://github.com/WICG/import-maps + +Below, we consider a few perspectives on this functionality. + +## Extension development + +If you develop an extension which includes ESM files, then you may want to add items to the import-map: + +```php +function myext_civicrm_esmImportMap(array &$importMap, array $context): void { + $importMap['imports']['foo/'] = E::url('js/foo/'); + $importMap['imports']['bar/'] = E::url('packages/bar/dist/'); +} +``` + +Then, you can write JS code which imports from these logical-paths, e.g. + +```php +Civi::resources()->addModule(' + import { FooClass } from "foo/foo.dist.js"; // Maps to MY_EXTENSION/js/foo/foo.dist.js + import { barFunc } from "bar/bar.funcs.js"; // Maps to MY_EXTENSION/packages/bar/dist/bar.funcs.js + barFunc("Hello", new FooClass()); +'); +``` + +## Loader development + +The *loader* is responsible for reading the `$importMap` and making it available to the browser. There are +currently two loader implementations. The future may require more. But before discussing details, let's consider why. + +### State of the ecosystem + +At time of writing (early/mid-2023), adoption of browser-based imports/import-maps is in a middle stage: + +* We're coming out of a period where browser support was negligble. Heretofore, module-loading has been implemented by + a rotating cast of third-party loaders (Webpack, Rollup, SystemJS, RequireJS, etc). These have been awkward to + integrate with PHP application-frameworks (CiviCRM, Drupal, WordPress, etc) due to dependency/build/workflow issues. + +* All major browsers now publish stable-releases with native support for `module` and `importmap`. This provides a + better basis for PHP application-frameworks to use ESM. This is cause for optimism. The `importmap` model should + have good (and improving) support over time. However, older browsers are still around. + +* The PHP application-frameworks that we support (Drupal, WordPress, Joomla, Backdrop) have not yet defined services or + conventions for `importmap`s. Over time, each may adopt slightly different conventions. Additionally, these + frameworks are pluggable -- in absence of a framework-convention, other plugins may adopt their own conventions. + +* The browser standards provide a common model, and we should expect this model to influence future updates throughout + the ecosystem. But it doesn't guarantee interoperability within PHP ecosystem -- future releases (of any framework + or any plugin) could introduce incompatibilities. We cannot give good solutions for incompatibilities that don't + exist yet. + +* To my mind, the major risks are: + * Multiple parties may output `\n"; + ``` +2. Given that CiviCRM (as a whole; core+extensions) has defined an import-map, ensure that the import-map is properly + loaded so that recursive-dependencies may be resolved. For example: + ```php + $civicrmImportMap = json_encode(Civi::service('esm.import_map')->get()); + echo "\n"; + ``` + +There are a few variations on how to perform these steps. Each variant defines a service `esm.loader.XXX` +(implemented in `Civi\Esm\XXX`). For example, these two are currently implemented: + +* `esm.loader.browser` (`Civi\Esm\BrowserLoader`): Use pure, browser-based loading with `