ESM nativo e import maps

Carga módulos publicados en un navegador con módulos ES nativos: el shell del release con su import map en línea y los módulos inmediatos precargados, o un import map que construyes a partir de un documento de resolución.

  • Disponibilidad: Experimental
  • Evidencia: Ejecución registrada
  • Guía práctica

Estado inicial

Una aplicación elige un modo de carga, esm o system. esm es el predeterminado: el navegador carga los módulos de forma nativa y un import map le indica dónde está cada especificador público. Esta página cubre ese modo. Para el otro, consulta SystemJS, y para todos los entornos lado a lado, Estrategias por entorno.

Necesitas un release cuyo target de frontend se haya preparado con target=browser y format=esm, y el documento de resolución de ese release.

Por qué un import map

Los módulos compilados conservan sus imports de otros módulos públicos como referencias simples:

JavaScript
import { route } from '@example/app/core/router';

Un navegador no puede obtener un especificador simple. Un import map le da la URL, de modo que el código de la aplicación nunca contiene una versión ni un origen, y dos releases de la misma aplicación solo difieren en su mapa.

Construye el mapa a partir de la resolución

Los valores de un documento beyond-resolution/1 son relativos al origen. Un navegador resuelve las direcciones relativas de un import map contra la página, que rara vez es el origen que sirve los módulos, así que escribe URL absolutas anteponiendo tu origen base:

HTMLimportmap.html
<!doctype html>
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<title>Native ESM with an import map</title>
		<script>
			// The only value that changes between a development server and the CDN.
			// Pass ?origin=http://localhost:<port> to load the same page against another origin.
			const origin = new URLSearchParams(location.search).get('origin') ?? 'https://cdn.example';

			// Origin-relative values, exactly as a beyond-resolution/1 document lists them
			const query = 'target=browser&format=esm&env=production&min=true&sourcemap=external&types=false&css=false';
			const imports = {
				'@example/shared/text': `/m/@example/shared@1.0.0/modules/text?${query}`
			};

			// An import map must be in the document before the first module loads, and a browser
			// resolves its relative addresses against the page, so write absolute URLs.
			const map = document.createElement('script');
			map.type = 'importmap';
			map.textContent = JSON.stringify({
				imports: Object.fromEntries(Object.entries(imports).map(([specifier, path]) => [specifier, origin + path]))
			});
			document.currentScript.after(map);
		</script>
	</head>
	<body>
		<pre id="output">Loading…</pre>
		<script type="module">
			// The application imports the public specifier. It never writes a version or an origin.
			const module = await import('@example/shared/text');
			document.getElementById('output').textContent = Object.keys(module).join('\n');
		</script>
	</body>
</html>

En el host de una aplicación publicada no escribes esta página tú. El release lleva un shell HTML generado:

  1. las hojas de estilos inmediatas, enlazadas en la cabecera;
  2. el import map, en línea, construido a partir de la resolución congelada con el prefijo del release como origen base (Resolution.importmap('/_r/<release number>')), para que funcionen los enlaces profundos;
  3. <link rel="modulepreload"> para cada módulo inmediato, justo después del mapa, para que el navegador los solicite en paralelo en lugar de descubrirlos de a un import por vez;
  4. un bootstrap de módulo que importa la entrada por su especificador público y enlaza las hojas de estilos diferidas cuando la entrada ya cargó.

El shell, el import map y el bootstrap se generan una vez por release y por target y se guardan como artefactos; nada se genera en una solicitud. Consulta el prefijo del release.

Con el códec compartido, el mismo paso es una sola llamada:

JavaScript
import { Resolution } from '@beyond-js/artifact-api';

const resolution = Resolution.parse(text);
const map = resolution.importmap(origin); // { imports, scopes } with absolute URLs

scopes es la forma en que conviven dos versiones de un mismo paquete: los módulos bajo un prefijo de ámbito resuelven un especificador de manera distinta que el resto. Conserva los ámbitos al construir el mapa.

Un origen también responde el propio mapa en <base>/importmap.json?target=browser&format=esm, con direcciones relativas a esa URL. Un navegador no carga un import map externo (<script type="importmap" src> no está soportado por los navegadores), así que una página incluye en línea direcciones absolutas, como arriba, o es el shell del release.

Resultado esperado

El navegador solicita cada módulo al origen base con la consulta canónica, y cada respuesta es application/javascript con un ETag fuerte. Los módulos de carga diferida se solicitan cuando la aplicación los importa, desde el mismo release.

Límites

Si falla

Síntoma Causa probable
Failed to resolve module specifier El especificador no está en el mapa, o el mapa se agregó después de que ya se había cargado un módulo
404 OUTPUT_NOT_AVAILABLE El release no se preparó con este target, formato o conjunto de opciones
401 ACCESS_REQUIRED La aplicación es privada; consulta Acceso privado
Un módulo carga una versión inesperada de una dependencia Los scopes de la resolución se perdieron al construir el mapa