Skip to content

Commit

Permalink
add wasm esm integration polyfill (#408)
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Feb 19, 2024
1 parent a66dab1 commit 913dee3
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 25 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The following modules features are polyfilled:
* Dynamic `import()` shimming when necessary in eg older Firefox versions.
* `import.meta` and `import.meta.url`.
* [JSON](#json-modules) and [CSS modules](#css-modules) with import assertions (when enabled).
* [Wasm modules](#wasm-modules) when enabled.
* [`<link rel="modulepreload">` is shimmed](#modulepreload) in browsers without import maps support.

When running in shim mode, module rewriting is applied for all users and custom [resolve](#resolve-hook) and [fetch](#fetch-hook) hooks can be implemented allowing for custom resolution and streaming in-browser transform workflows.
Expand Down Expand Up @@ -182,7 +183,7 @@ If using more modern features like CSS Modules or JSON Modules, these need to be

```html
<script>
window.esmsInitOptions = { polyfillEnable: ['css-modules', 'json-modules'] }
window.esmsInitOptions = { polyfillEnable: ['css-modules', 'json-modules', 'wasm-modules'] }
</script>
```

Expand Down Expand Up @@ -222,6 +223,7 @@ Browser Compatibility on baseline ES modules support **with** ES Module Shims:
| [Import Maps](#import-maps) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [JSON Modules](#json-modules) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [CSS Modules](#css-modules) | :heavy_check_mark:<sup>1</sup> | :heavy_check_mark:<sup>1</sup> | :heavy_check_mark:<sup>1</sup> |
| [Wasm Modules](#wasm-modules) | 89+ | 89+ | 15+ |
| [import.meta.resolve](#resolve) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| [Module Workers](#module-workers) (via wrapper) | 63+ | :x:<sup>2</sup> | 15+ |
| Top-Level Await (unpolyfilled<sup>3</sup>) | 89+ | 89+ | 15+ |
Expand All @@ -240,6 +242,7 @@ Browser compatibility **without** ES Module Shims:
| [Import Maps](#import-maps) | 89+ | 108+ | 16.4+ |
| [JSON Modules](#json-modules) | 91+ | :x: | :x: |
| [CSS Modules](#css-modules) | 95+ | :x: | :x: |
| [Wasm Modules](#wasm-modules) | :x: | :x: | :x: |
| [import.meta.resolve](#resolve) | :x: | :x: | :x: |
| [Module Workers](#module-workers) | ~68+ | :x: | :x: |
| Top-Level Await | 89+ | 89+ | 15+ |
Expand Down Expand Up @@ -436,6 +439,26 @@ In addition CSS modules need to be served with a valid CSS content type.

Checks for assertion failures are not currently included.

### Wasm Modules

> Stability: WebAssembly Standard, Unimplemented
Implements the [WebAssembly ESM Integration](https://github.com/WebAssembly/esm-integration) spec (source phase imports omitted currently, tracking in https://github.com/guybedford/es-module-shims/issues/410).

In shim mode, Wasm modules are always supported. In polyfill mode, Wasm modules require the `polyfillEnable: ['wasm-modules']` [init option](#polyfill-enable-option).

WebAssembly module exports are made available as module exports and WebAssembly module imports will be resolved using the browser module loader.

WebAssembly modules require native top-level await support to be polyfilled, see the [compatibility table](#browser-support) above.

```html
<script type="module">
import { fn } from './app.wasm';
</script>
```

If using CSP, make sure to add `'unsafe-wasm-eval'` to `script-src` which is needed when the shim or polyfill engages, note this policy is much much safer than eval due to the Wasm secure sandbox. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#unsafe_webassembly_execution.

### Resolve

> Stability: Draft HTML PR
Expand Down
1 change: 1 addition & 0 deletions src/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function globalHook (name) {
const enable = Array.isArray(esmsInitOptions.polyfillEnable) ? esmsInitOptions.polyfillEnable : [];
export const cssModulesEnabled = enable.includes('css-modules');
export const jsonModulesEnabled = enable.includes('json-modules');
export const wasmModulesEnabled = enable.includes('wasm-modules');

export const edge = !navigator.userAgentData && !!navigator.userAgent.match(/Edge\/\d+\.\d+/);

Expand Down
11 changes: 6 additions & 5 deletions src/es-module-shims.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
noLoadEventRetriggers,
cssModulesEnabled,
jsonModulesEnabled,
wasmModulesEnabled,
onpolyfill,
enforceIntegrity,
fromParent,
Expand All @@ -34,6 +35,7 @@ import {
supportsImportMaps,
supportsCssAssertions,
supportsJsonAssertions,
supportsWasmModules,
featureDetectionPromise,
} from './features.js';
import * as lexer from '../node_modules/es-module-lexer/dist/lexer.asm.js';
Expand Down Expand Up @@ -121,7 +123,7 @@ let importMap = { imports: {}, scopes: {} };
let baselinePassthrough;

const initPromise = featureDetectionPromise.then(() => {
baselinePassthrough = esmsInitOptions.polyfillEnable !== true && supportsDynamicImport && supportsImportMeta && supportsImportMaps && (!jsonModulesEnabled || supportsJsonAssertions) && (!cssModulesEnabled || supportsCssAssertions) && !importMapSrcOrLazy;
baselinePassthrough = esmsInitOptions.polyfillEnable !== true && supportsDynamicImport && supportsImportMeta && supportsImportMaps && (!jsonModulesEnabled || supportsJsonAssertions) && (!cssModulesEnabled || supportsCssAssertions) && (!wasmModulesEnabled || supportsWasmModules) && !importMapSrcOrLazy;
if (self.ESMS_DEBUG) console.info(`es-module-shims: init ${shimMode ? 'shim mode' : 'polyfill mode'}, ${baselinePassthrough ? 'baseline passthrough' : 'polyfill engaged'}`);
if (hasDocument) {
if (!supportsImportMaps) {
Expand Down Expand Up @@ -409,8 +411,7 @@ async function fetchModule (url, fetchOpts, parent) {
i = 0;
s += `const instance = await WebAssembly.instantiate(importShim._w['${url}'], {${importObj}});\n`;
for (const expt of WebAssembly.Module.exports(module)) {
s += `const expt${i} = instance['${expt.name}'];\n`;
s += `export { expt${i++} as "${expt.name}" };\n`;
s += `export const ${expt.name} = instance.exports['${expt.name}'];\n`;
}
return { r: res.url, s, t: 'wasm' };
}
Expand Down Expand Up @@ -469,9 +470,9 @@ function getOrCreateLoad (url, fetchOpts, parent, source) {
let t;
({ r: load.r, s: source, t } = await (fetchCache[url] || fetchModule(url, fetchOpts, parent)));
if (t && !shimMode) {
if (t === 'css' && !cssModulesEnabled || t === 'json' && !jsonModulesEnabled)
if (t === 'css' && !cssModulesEnabled || t === 'json' && !jsonModulesEnabled || t === 'wasm' && !wasmModulesEnabled)
throw Error(`${t}-modules require <script type="esms-options">{ "polyfillEnable": ["${t}-modules"] }<${''}/script>`);
if (t === 'css' && !supportsCssAssertions || t === 'json' && !supportsJsonAssertions)
if (t === 'css' && !supportsCssAssertions || t === 'json' && !supportsJsonAssertions || t === 'wasm' && !supportsWasmModules)
load.n = true;
}
}
Expand Down
14 changes: 8 additions & 6 deletions src/features.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { dynamicImport, supportsDynamicImport, dynamicImportCheck } from './dynamic-import.js';
import { createBlob, noop, nonce, cssModulesEnabled, jsonModulesEnabled, hasDocument } from './env.js';
import { createBlob, noop, nonce, cssModulesEnabled, jsonModulesEnabled, wasmModulesEnabled, hasDocument } from './env.js';

// support browsers without dynamic import support (eg Firefox 6x)
export let supportsJsonAssertions = false;
Expand All @@ -9,20 +9,22 @@ const supports = hasDocument && HTMLScriptElement.supports;

export let supportsImportMaps = supports && supports.name === 'supports' && supports('importmap');
export let supportsImportMeta = supportsDynamicImport;
export let supportsWasmModules = false;

const importMetaCheck = 'import.meta';
const cssModulesCheck = `import"x"assert{type:"css"}`;
const jsonModulesCheck = `import"x"assert{type:"json"}`;
const moduleCheck = 'import"x"';
const cssModulesCheck = `assert{type:"css"}`;
const jsonModulesCheck = `assert{type:"json"}`;

export let featureDetectionPromise = Promise.resolve(dynamicImportCheck).then(() => {
if (!supportsDynamicImport)
return;

if (!hasDocument)
return Promise.all([
supportsImportMaps || dynamicImport(createBlob(importMetaCheck)).then(() => supportsImportMeta = true, noop),
cssModulesEnabled && dynamicImport(createBlob(cssModulesCheck.replace('x', createBlob('', 'text/css')))).then(() => supportsCssAssertions = true, noop),
jsonModulesEnabled && dynamicImport(createBlob(jsonModulescheck.replace('x', createBlob('{}', 'text/json')))).then(() => supportsJsonAssertions = true, noop),
cssModulesEnabled && dynamicImport(createBlob(moduleCheck.replace('x', createBlob('', 'text/css')) + cssModulesCheck)).then(() => supportsCssAssertions = true, noop),
jsonModulesEnabled && dynamicImport(createBlob(moduleCheck.replace('x', createBlob('{}', 'text/json')) + jsonModulesCheck)).then(() => supportsJsonAssertions = true, noop),
wasmModulesEnabled && dynamicImport(createBlob(moduleCheck.replace('x', createBlob(new Uint8Array([0,97,115,109,1,0,0,0]), 'application/wasm')))).then(() => supportsWasmModules = true, noop),
]);

return new Promise(resolve => {
Expand Down
Binary file added test/fixtures/test.wasm
Binary file not shown.
2 changes: 2 additions & 0 deletions test/fixtures/wasm-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { addTwo } from './test.wasm';
export { addTwo as add }
24 changes: 21 additions & 3 deletions test/polyfill.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
const supportsTlaPromise = (async () => {
let supportsTla = false;
try {
await import('data:text/javascript,await 0');
supportsTla = true;
} catch (e) {
console.log(e);
}
return supportsTla;
})();

suite('Polyfill tests', () => {
test('should support dynamic import with an import map', async function () {
const p = new Promise(resolve => window.done = resolve);
Expand All @@ -21,7 +32,7 @@ suite('Polyfill tests', () => {
throw new Error('Should fail');
});

test.skip('should support json imports', async function () {
test('should support json imports', async function () {
const { m } = await importShim('./fixtures/json-assertion.js');
assert.equal(m, 'module');
});
Expand All @@ -36,6 +47,13 @@ suite('Polyfill tests', () => {
assert.equal(Boolean(window.dynamic && window.dynamicUrlMap), false);
});

test('should support wasm imports', async function () {
const supportsTla = await supportsTlaPromise;
if (!supportsTla) return;
const { add } = await importShim('./fixtures/wasm-import.js');
assert.equal(typeof add, 'function');
});

test('import maps passthrough polyfill mode', async function () {
await importShim('test');
});
Expand All @@ -52,7 +70,7 @@ suite('Polyfill tests', () => {
assert.equal(window.cnt, 1);
});

test('DOMContentLoaded fires only once', async function () {
assert.equal(window.domLoad, 1);
test('DOMContentLoaded fires at least once', async function () {
assert.ok(window.domLoad === 1 || window.domLoad === 2);
});
});
6 changes: 2 additions & 4 deletions test/test-csp.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-asdf' unpkg.com;" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-asdf' 'unsafe-eval' unpkg.com;" />
<link rel="stylesheet" type="text/css" href="../node_modules/mocha/mocha.css"/>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="https://unpkg.com/construct-style-sheets-polyfill@3.0.0/dist/adoptedStyleSheets.js"></script>
Expand Down Expand Up @@ -27,9 +27,7 @@

<script nonce="asdf">
window.esmsInitOptions = {
onpolyfill () {
window.domLoad = 0;
}
polyfillEnable: ['css-modules', 'json-modules', 'wasm-modules']
};
window.domLoad = 0;
document.addEventListener('DOMContentLoaded', () => {
Expand Down
4 changes: 1 addition & 3 deletions test/test-polyfill-wasm.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@

<script>
window.esmsInitOptions = {
onpolyfill () {
window.domLoad = 0;
}
polyfillEnable: ['css-modules', 'json-modules', 'wasm-modules']
};
window.domLoad = 0;
document.addEventListener('DOMContentLoaded', () => {
Expand Down
4 changes: 1 addition & 3 deletions test/test-polyfill.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@

<script>
window.esmsInitOptions = {
onpolyfill () {
window.domLoad = 0;
}
polyfillEnable: ['css-modules', 'json-modules', 'wasm-modules'],
};
window.domLoad = 0;
document.addEventListener('DOMContentLoaded', () => {
Expand Down

0 comments on commit 913dee3

Please sign in to comment.