Skip to main content
Version: main (5.2)

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 a pre_render hook.

Built-in specifiers

The following specifiers are registered by default in add_standard_imports():

SpecifierResolves to
@moodle/lms/ESM endpoint root (Moodle component modules)
reactexternal/react
react-domexternal/react-dom
react/jsx-runtimeexternal/react/jsx-runtime
react/jsx-dev-runtimeexternal/react/jsx-dev-runtime
@moodlehq/design-systemexternal/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 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
reactlib/platform_bundles/react/<version>/react.js
react-domlib/platform_bundles/react/<version>/react-dom-client.js
react/jsx-runtimelib/platform_bundles/react/<version>/jsx-runtime.js
react/jsx-dev-runtimelib/platform_bundles/react/<version>/jsx-dev-runtime.js
design-systemlib/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:

RevisionBehaviour
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 <component_directory>/js/react/build/<module>.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:
// 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.

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:

$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