From 2e8f4b70f1dff01f4c637827cf2fede5d1942455 Mon Sep 17 00:00:00 2001 From: meirzamoodle Date: Sun, 1 Mar 2026 07:34:19 +0700 Subject: [PATCH] MDL-87922 [docs] Add guide for JavaScript import maps in Moodle Adds a new documentation page explaining how Moodle uses native browser import maps to resolve bare module specifiers (e.g. `react`, `@moodle/lms/`) to real URLs at runtime. The guide covers: - What an import map is and how the browser uses it - How Moodle builds and injects the import map via `page_requirements_manager` - The `core\output\requirements\import_map` class and its built-in specifiers (React, react-dom, jsx-runtime, and the Moodle design system) - How to register custom or plugin specifiers from a `pre_render` hook - The ESM serving endpoint and how it resolves external bundles vs component React builds - The HTTP caching strategy (immutable headers for versioned revisions, short-lived for `-1` development mode) - How to author a component React module and expose it through the map - How to override a built-in specifier (e.g. swap in a debug React build) --- docs/guides/javascript/react/importmap.md | 174 ++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 docs/guides/javascript/react/importmap.md diff --git a/docs/guides/javascript/react/importmap.md b/docs/guides/javascript/react/importmap.md new file mode 100644 index 0000000000..1aef0ee4cb --- /dev/null +++ b/docs/guides/javascript/react/importmap.md @@ -0,0 +1,174 @@ +--- +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 { 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 central registry that holds all specifier-to-URL mappings. It implements `JsonSerializable` so it can be written directly into the page as JSON. + +Key responsibilities: + +- Holds a list of specifier → loader entries. +- Accepts a **default loader URL** (the ESM serving endpoint) which is used to derive concrete URLs for all entries whose path is relative. +- 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 | Resolves to | +|---|---| +| `@moodle/lms/` | ESM endpoint root (Moodle component modules) | +| `react` | `external/react` | +| `react-dom` | `external/react-dom` | +| `react/jsx-runtime` | `external/react/jsx-runtime` | +| `react/jsx-dev-runtime` | `external/react/jsx-dev-runtime` | +| `@moodlehq/design-system` | `external/design-system` | + +#### 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. +$importmap->add_import('my-lib', loader: new \core\url('https://cdn.example.com/my-lib.js')); + +// Map '@myplugin/' using a path relative to the default ESM loader root. +$importmap->add_import('@myplugin/', path: 'local_myplugin/'); +``` + +The `add_import()` signature is: + +```php +public function add_import( + string $specifier, + ?\core\url $loader = null, + ?string $path = null, +): void +``` + +- **`$specifier`** — The bare specifier string used in `import` statements (e.g. `react`, `@moodle/lms/`). +- **`$loader`** — An absolute `\core\url`. When provided, used directly. +- **`$path`** — A path suffix appended to the default loader base URL. When both `$loader` and `$path` are `null`, `$specifier` itself is appended to the base URL. + +## The ESM serving endpoint + +All ESM files are served by `core\route\shim\esm_controller::serve`, registered under the route: + +``` +/{revision:[0-9-]+}/{scriptpath:.*} +``` + +The controller dispatches on the value of `scriptpath`: + +### External (vendored) bundles — `external/` + +Paths that begin with `external/` are resolved against a fixed map of vendored files stored under `$CFG->root/lib/platform_bundles/`: + +| Specifier (after `external/`) | File | +|---|---| +| `react` | `lib/platform_bundles/react//react.js` | +| `react-dom` | `lib/platform_bundles/react//react-dom-client.js` | +| `react/jsx-runtime` | `lib/platform_bundles/react//jsx-runtime.js` | +| `react/jsx-dev-runtime` | `lib/platform_bundles/react//jsx-dev-runtime.js` | +| `design-system` | `lib/platform_bundles/moodle-design-system//index.js` | + +### Component React builds — everything else + +Any `scriptpath` that does not start with `external/` is treated as a `/` alias and resolved to: + +``` +/react/build/.js +``` + +For example, `mod_book/viewer` resolves to `/mod/book/react/build/viewer.js`. + +## HTTP caching + +The `esm_script_serving` trait, mixed into the controller, applies the following caching strategy: + +| Revision | Behaviour | +|---|---| +| Valid (positive integer) | Long-lived immutable cache headers + ETag. Returns `304 Not Modified` when the client already has the file cached. | +| `-1` (development / invalid) | Short-lived cache headers. Forces the browser to re-fetch on every page load. | + +The revision value comes from `page_requirements_manager::get_jsrev()` and changes whenever JavaScript files are updated, automatically busting caches. + +## Writing a component React module + +To expose a React module through the import map: + +1. Build your TypeScript/React source (`.ts` or `.tsx`) into a compiled JavaScript file at `/js/react/build/.js` using the Moodle build tools. See the React build tooling guide for details. +2. Import it in browser code using the `@moodle/lms/` scope: + +```js +// In any ES module on the page: +import { BookViewer } from '@moodle/lms/mod_book/viewer'; +``` + +The import map translates `@moodle/lms/mod_book/viewer` → the ESM endpoint URL for `mod_book/viewer`, which the controller then resolves to `/mod/book/js/react/build/viewer.js`. + +:::note +The `@moodle/lms/` specifier ends with a trailing slash. This is the standard import map convention for **package scopes**: any import that begins with `@moodle/lms/` will be prefixed with the loader base URL, allowing the entire namespace to be served from one endpoint without registering each module individually. +::: + +## Overriding a built-in specifier + +You can replace any of the default specifiers in a `pre_render` hook. For example, to swap in a local React build during development: + +```php +$importmap = \core\di::get(\core\output\requirements\import_map::class); +$importmap->add_import( + 'react', + loader: new \core\url('/local/devtools/react-debug.js'), +); +``` + +Calling `add_import()` with the same specifier twice overwrites the previous entry, so ordering matters when multiple hooks are involved. + +## See also + +- [JavaScript Modules](../modules.md) — AMD and ESM module authoring in Moodle. +- [MDN: Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) +- [HTML spec: import maps](https://html.spec.whatwg.org/multipage/webappapis.html#import-maps)