Aliasing and Import Maps
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 <script type="importmap"> tag, that tells the browser how to resolve bare specifiers used in import statements.
<script type="importmap">
{
"imports": {
"@moodle/lms/": "http://example.com/r.php/-1/",
"react": "http://example.com/r.php/-1/external/react",
}
}
</script>
With this map in place, any ES module on the page can write:
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 <head> 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 apre_renderhook.
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:
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:
public function add_import(
string $specifier,
?\core\url $loader = null,
?string $path = null,
): void
$specifier— The bare specifier string used inimportstatements (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$loaderand$patharenull,$specifieritself 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/<version>/react.js |
react-dom | lib/platform_bundles/react/<version>/react-dom-client.js |
react/jsx-runtime | lib/platform_bundles/react/<version>/jsx-runtime.js |
react/jsx-dev-runtime | lib/platform_bundles/react/<version>/jsx-dev-runtime.js |
design-system | lib/platform_bundles/moodle-design-system/<version>/index.js |
Component React builds — everything else
Any scriptpath that does not start with external/ is treated as a <component>/<module> alias and resolved to:
<component_directory>/react/build/<module>.js
For example, mod_book/viewer resolves to <dirroot>/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:
- Build your TypeScript/React source (
.tsor.tsx) into a compiled JavaScript file at<component_directory>/js/react/build/<module>.jsusing the Moodle build tools. See the React build tooling guide for details. - Import it in browser code using the
@moodle/lms/scope:
// 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 <dirroot>/mod/book/js/react/build/viewer.js.
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:
$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 — AMD and ESM module authoring in Moodle.
- MDN: Import maps
- HTML spec: import maps