diff --git a/docs/guides/javascript/react/importmap.md b/docs/guides/javascript/react/importmap.md new file mode 100644 index 0000000000..c1953adb2b --- /dev/null +++ b/docs/guides/javascript/react/importmap.md @@ -0,0 +1,222 @@ +--- +title: Aliasing and Import Maps +tags: + - Javascript + - ESM + - React + - Import Maps +description: How Moodle uses native browser import maps to resolve bare module specifiers to real URLs at runtime, including built-in specifiers, custom entries, and the ESM serving endpoint. +--- + +Moodle uses native browser import maps as the mechanism for resolving bare module specifiers +(for example `react` or `@moodle/lms/`) to real URLs at runtime. This replaces the need for +bundler-specific alias configuration and allows Moodle components to write standard ESM +`import` statements that work directly in the browser. + +## What is an Import Map? + +An import map is a JSON object, embedded in the page as a ` +``` + +With this map in place, any ES module on the page can write: + +```js +import React from 'react'; +import { jsx } from 'react/jsx-runtime'; +import { someUtil } from '@moodle/lms/core/utils'; +``` + +…and the browser resolves the specifier to the correct URL without any bundler step at runtime. + +## How Moodle generates the Import Map + +The import map is built and injected into the page automatically by +`page_requirements_manager::get_import_map()`, which is called during the page `
` +render phase. + +### The `import_map` class + +**`core\output\requirements\import_map`** is the single source of truth for all +specifier → URL mappings and specifier → filesystem path mappings. It implements +`JsonSerializable` so it can be written directly into the page as JSON, and it is also +consulted by the ESM controller when serving files. + +Key responsibilities: + +- Holds a list of specifier → entry mappings. +- Accepts a **default loader URL** (the ESM serving endpoint) which is used to derive + concrete URLs for entries that use the default loader. +- Provides `add_import()` to register additional specifiers, or to override the built-in + ones, from a `pre_render` hook. + +#### Built-in specifiers + +The following specifiers are registered by default in `add_standard_imports()`: + +| Specifier | URL in import map | Filesystem path | +|---|---|---| +| `@moodle/lms/` | `{loaderBase}@moodle/lms/` | Component's `js/esm/build/` directory | +| `@moodlehq/design-system` | `{loaderBase}@moodlehq/design-system` | `lib/js/bundles/design-system.js` | +| `react` | `{loaderBase}react` | `lib/js/bundles/react/react.js` | +| `react/` | `{loaderBase}react/` | `lib/js/bundles/react/` (prefix) | +| `react-dom` | `{loaderBase}react-dom` | `lib/js/bundles/react-dom/react-dom.js` | + +The `react/` prefix entry covers all sub-specifiers such as `react/jsx-runtime`. + +:::note +`{loaderBase}` is the base URL of the ESM serving endpoint — the URL that +`page_requirements_manager::get_import_map()` sets as the default loader. In practice it looks like +`https://example.com/esm/12345/`, where `12345` is the JS revision number returned by +`page_requirements_manager::get_jsrev()`. All specifier URLs in the import map are built by +appending the bare specifier to this base URL. + +The revision value `/esm/-1/` means the revision is invalid or in development mode. When the +revision is `-1`, the ESM controller applies short-lived cache headers so the browser re-fetches +the file on every page load instead of serving a stale cached copy. +::: + +#### Adding a custom specifier + +You can extend the import map from a `pre_render` hook before the page is rendered: + +```php +use core\output\requirements\import_map; + +// Fetch the shared singleton from the DI container. +$importmap = \core\di::get(import_map::class); + +// Map 'my-lib' to an absolute URL (e.g. a CDN). Used verbatim in the import map. +$importmap->add_import('my-lib', loader: new \core\url('https://cdn.example.com/my-lib.js')); + +// Map '@myplugin/' using a filesystem path relative to $CFG->root. +// The URL in the import map will be '{loaderBase}@myplugin/'. +// The ESM controller uses $path to locate the file on disk. +$importmap->add_import('@myplugin/', path: 'local_myplugin/js/esm/build'); +``` + +The `add_import()` signature is: + +```php +public function add_import( + string $specifier, + ?\core\url $loader = null, + ?string $path = null, + bool $loadfromcomponent = false, +): void +``` + +- **`$specifier`** — The bare specifier string used in `import` statements + (e.g. `react`, `@moodle/lms/`). +- **`$loader`** — An absolute `\core\url`. When provided, this URL is written directly + into the import map and `$path` is ignored. +- **`$path`** — A filesystem path relative to `$CFG->root`, used by the ESM controller + to locate the file on disk. Has no effect on the URL written into the import map + (the URL always uses `{loaderBase}{specifier}`). +- **`$loadfromcomponent`** — When `true`, the specifier is treated as a + `