Skip to content

Announcing ES Module Shims 2.0

Refer

Source: Announcing ES Module Shims 2.0

Author: Guy Bedford

Translator: SenaoXi

Release Time: February 24, 2025

Copyright Statement

Translation and Republication Notice:

This translation is provided for educational and informational purposes only. All intellectual property rights, including copyright, remain with the original author and/or publisher. This translation maintains the integrity of the original content while making it accessible to chinese readers.

Modifications Disclosure:

  • This is a complete and faithful translation of the original content with no substantive modifications.
  • This translation includes minor adaptations to improve clarity for chinese readers while preserving all essential information and viewpoints.
  • Sections marked with [†] contain supplementary explanations added by the translator to provide cultural or technical context.

Rights Reservation:

If you are the copyright holder and believe this translation exceeds fair use guidelines, please contact us at email. We are committed to respecting intellectual property rights and will promptly address any legitimate concerns.

ES Module Shims 2.0 is now officially released, a comprehensive 13KB lightweight polyfill that supports import maps, multiple import maps, CSS and JSON imports, WebAssembly modules, and Source Phase imports.

If you're not familiar with what these features are, they will be explained in detail below. But first, I want to highlight an important new feature in version 2.0: TypeScript type erasure support.

TypeScript Type Erasure Support

Why compile TypeScript in the browser? The context is that ES Module Shims is a very fast module polyfill designed to fill in all new module features on browsers that support basic module systems. Therefore, the TC39 type annotations proposal fits perfectly within this project's polyfill definition. Additionally, by using the TypeScript variant defined in the Amaro project from Node.js in the browser, providing direct source-by-source rewriting (type erasure or erasable syntax), we actually get a very fast workflow.

It implements a buildless workflow - TypeScript as the final piece, providing a streamlined Web development approach without build tools, Node.js, or npm.

Here's a practical example, a Vue component written in TypeScript:

typescript
import { defineComponent } from 'vue';
import style from './user-card.css' with { type: 'css' };

document.adoptedStyleSheets.push(style);

export interface User {
  name: string;
  age: number;
}

export default defineComponent({
  props: {
    user: {
      type: Object as () => User,
      required: true
    }
  },
  template: `<div class="user-card">{{ user.name }} <span class="age">({{ user.age }})</span></div>`
});

In the code above, we're not only using TypeScript but also the newly supported CSS Module Scripts feature to modularly load the component's CSS:

css
.user-card {
  padding: 1.2rem;
  border-radius: 16px;
  margin: 1rem;
  font: 500 18px system-ui;
  width: 300px;
  background: linear-gradient(135deg, #eee 0%, #fafafa 100%);
  box-shadow: 2px 5px 7px rgba(100, 100, 255, 0.2);
  transition: transform 0.2s ease;
  cursor: pointer;
}
.user-card:hover {
  transform: translateY(-2px);
}
.age {
  color: #726497;
}

Now, using ES Module Shims, we can run this application with a single HTML file and static files, with polyfill support for both CSS Module Scripts and TypeScript, without any build process or additional steps:

html
<!doctype html>

<!-- Load ES Module Shims from your chosen CDN -->
<script
  async
  src="https://ga.jspm.io/npm:es-module-shims@2.0.9/dist/es-module-shims.js"
></script>

<!-- Enable TypeScript and CSS import features (by default only import maps are polyfilled) -->
<script type="esms-options">
  { "polyfillEnable": ["typescript", "css-modules"] }
</script>

<!-- Set up dependencies in the import map -->
<script type="importmap">
  {
    "imports": {
      "vue": "https://ga.jspm.io/npm:vue@3.5.13/dist/vue.esm-browser.prod.js"
    }
  }
</script>

<div id="app">
  <user-card v-for="user in users" :key="user.name" :user="user" />
</div>

<!-- ES Module Shims will find this and handle the rest -->
<script type="module" lang="ts">
  import { createApp } from 'vue';
  import UserCard, { type User } from './user-card.ts';

  createApp({
    setup() {
      const users: User[] = [
        { name: 'Alice', age: 25 },
        { name: 'Bob', age: 30 }
      ];
      return { users };
    }
  })
    .component('user-card', UserCard)
    .mount('#app');
</script>

You can view the complete example here.

Support for Multiple Import Maps

Anyone who has used import maps for a while will know the dreaded "An import map is added after module script load was triggered." error. Thanks to Yoav Weiss's hard work, we now have support for multiple import maps in the latest version of Chrome.

ES Module Shims 2.0 includes a polyfill for this feature that detects when multiple import maps are used, then checks if modules depend on mappings that exist in the new import map but not in the old one, following the standard polyfill failure semantics. In practice, we can polyfill multiple import maps on top of single import map support, now able to share native module loaders and registries when possible, just as we polyfill import maps themselves based on non-import map module support in older browsers.

Additionally, by using mutation observers on the body and head tags, we can also detect when import maps are dynamically injected, then apply the same polyfill for dynamic loading workflows (provided new dynamic imports are loaded through the global importShim() polyfill).

Wasm Modules and Source Semantic Import Specification

Typically, WebAssembly is loaded using patterns like fetch('./module.wasm').then(WebAssembly.compileStreaming).then(...) to get a WebAssembly.Module object for initialization.

In these workflows, getting the URL for the Wasm binary isn't always straightforward due to baseURL semantics. Additionally, build tools may struggle to work well with this kind of code, often requiring runtime configuration of binary paths.

With the source semantic import specification, we can now directly import Wasm binaries in a portable way, which ES Module Shims fully supports when enabled:

bash
<!doctype html>

<script
  async
  src="https://ga.jspm.io/npm:es-module-shims@2.0.9/dist/es-module-shims.js"
></script>

<!-- Enable the WebAssembly and Source Phase features -->
<script type="esms-options">
  { "polyfillEnable": ["wasm-modules", "source-phase"] }
</script>

<script type="module">
  import source mod from './module.wasm';
  const { fn } = new WebAssembly.Instance(mod, ...options...);
</script>

Finally, if you're still curious about how ES Module Shims works, I previously wrote an article about how ES Module Shims became a production import maps polyfill.

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (67e3a12)