ES Module Shims 2.0 is now live, a comprehensive 13KB polyfill for import maps, multiple import maps, CSS & JSON imports, Wasm modules and Source Phase imports.
If you don't know what all of these features are, they are covered below, but first and most importantly I want to highlight one of the major new features in 2.0: TypeScript type stripping support.
TypeScript Type Stripping Support
Why compile TypeScript in the browser? The background here is that ES Module Shims is a very fast modules polyfill aiming to polyfill all new modules features in browsers on top of baseline modules support. So the TC39 Type Annotations proposal exactly falls under the polyfill definition of this project. Furthermore using the same well-defined variant of TypeScript from Node.js's Amaro project in the browser providing direct per-source rewriting called type stripping or erasable syntax we actually do get a very fast workflow out of it.
It completes the "nobuild" workflow - TypeScript being the last piece providing a streamlined approach to web development without build tools, Node.js or npm.
To show a practical example, here's a Vue component written in TypeScript:
user-card.ts
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 above, we are not only using TypeScript, but also the newly supported CSS Module Scripts feature to modularly load the component's CSS:
user-card.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 use just a single HTML file with static files to run this app polyfilling CSS Module Scripts and the TypeScript support without needing any build process or any other steps at all:
index.html
<!doctype html>
<!-- Load ES Module Shims from your CDN of choice -->
<script async src="https://ga.jspm.io/npm:es-module-shims@2.0.9/dist/es-module-shims.js"></script>
<!-- Enable the TypeScript and CSS Import features (only import maps are polyfilled by default) -->
<script type="esms-options">
{ "polyfillEnable": ["typescript", "css-modules"] }
</script>
<!-- Set dependencies in 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>
See the full example in action here.
Multiple Import Map Support
Anyone who's worked with import maps for a while will know about the dreaded "An import map is added after module script load was triggered." error. Thanks to the hard work of Yoav Weiss we now have support for multiple import maps coming in the latest version of Chrome.
ES Module Shims 2.0 includes a polyfill for this feature, detecting when multiple import maps are being used and then checking if modules rely on mappings from the new import map that weren't present in the old one per the standard polyfill failure semantics. Effectively we can polyfill multiple import maps on top of singular import maps support, now sharing the native module loader and registry wherever possible, just like we polyfill import maps itself on top of non-import maps modules support in old browsers.
In addition, using mutation observers on the body and head tags we can also detect when an import map is dynamically injected to then apply the same polyfilling for dynamic loading workflows (provided new dynamic imports go through the global `importShim()` polyfill handler).
Wasm Modules & Source Phase Imports
Typically, WebAssembly is loaded using a pattern like fetch('./module.wasm').then(WebAssembly.compileStreaming).then(...)
to get a handle to the WebAssembly.Module
object for initialization.
In these workflows, getting the URL to the Wasm binary isn't always trivial with baseURL semantics in play. In addition, build tools can have a hard time working well with this code requiring runtime configurations of the binary path in many cases as well.
With the source phase imports standard we can now directly import Wasm binaries in a portable way, and this is fully supported in ES Module Shims when enabling the feature:
<!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 } = await WebAssembly.instantiate(mod, ...options...);
</script>
Finally, if you're still curious about how ES Module Shims works, I wrote a previous post about how ES Module Shims became a production import maps polyfill.