Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Wasm files that import JS resources #13608

Merged
merged 13 commits into from Jan 1, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -13,6 +13,7 @@
- `[jest-environment-node]` fix non-configurable globals ([#13687](https://github.com/facebook/jest/pull/13687))
- `[@jest/expect-utils]` `toMatchObject` should handle `Symbol` properties ([#13639](https://github.com/facebook/jest/pull/13639))
- `[jest-resolve]` Add global paths to `require.resolve.paths` ([#13633](https://github.com/facebook/jest/pull/13633))
- `[jest-runtime]` Support Wasm files that import JS resources ([#13608](https://github.com/facebook/jest/pull/13608))
- `[jest-snapshot]` Make sure to import `babel` outside of the sandbox ([#13694](https://github.com/facebook/jest/pull/13694))

### Chore & Maintenance
Expand Down
2 changes: 1 addition & 1 deletion e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap
Expand Up @@ -10,7 +10,7 @@ Ran all test suites matching /native-esm-deep-cjs-reexport.test.js/i."

exports[`runs WebAssembly (Wasm) test with native ESM 1`] = `
"Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites matching /native-esm-wasm.test.js/i."
Expand Down
10 changes: 10 additions & 0 deletions e2e/native-esm/__tests__/native-esm-wasm.test.js
Expand Up @@ -6,6 +6,7 @@
*/

import {readFileSync} from 'node:fs';
import {jest} from '@jest/globals';
// file origin: https://github.com/mdn/webassembly-examples/blob/2f2163287f86fe29deb162335bccca7d5d95ca4f/understanding-text-format/add.wasm
// source code: https://github.com/mdn/webassembly-examples/blob/2f2163287f86fe29deb162335bccca7d5d95ca4f/understanding-text-format/add.was
import {add} from '../add.wasm';
Expand Down Expand Up @@ -54,3 +55,12 @@ test('imports from "data:application/wasm" URI with invalid encoding fail', asyn
import('data:application/wasm;charset=utf-8,oops'),
).rejects.toThrow('Invalid data URI encoding: charset=utf-8');
});

test('supports wasm files that import js resources (wasm-bindgen)', async () => {
globalThis.alert = jest.fn();

const {greet} = await import('../wasm-bindgen/index.js');
greet('World');

expect(globalThis.alert).toHaveBeenCalledWith('Hello, World!');
});
12 changes: 12 additions & 0 deletions e2e/native-esm/wasm-bindgen/index.js
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

// folder source: https://github.com/rustwasm/wasm-bindgen/tree/4f865308afbe8d2463968457711ad356bae63b71/examples/hello_world
// docs: https://rustwasm.github.io/docs/wasm-bindgen/examples/hello-world.html

import * as wasm from './index_bg.wasm';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import?

Copy link
Contributor Author

@kachkaev kachkaev Nov 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks so, yes. All three files in this folder are auto-generated and I’ve decided to keep them as is. This will be helpful in the future if we need to re-generate the example again.

Maybe this like has its meaning actually. It makes sure that the wasm file is imported before JS, which may affect the result. index_bg.wasm and index_bg.js import stuff from each other, bg stands for bindgen.

export * from './index_bg.js';
141 changes: 141 additions & 0 deletions e2e/native-esm/wasm-bindgen/index_bg.js
@@ -0,0 +1,141 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import * as wasm from './index_bg.wasm';

const lTextDecoder =
typeof TextDecoder === 'undefined'
? (0, module.require)('util').TextDecoder
: TextDecoder;

const cachedTextDecoder = new lTextDecoder('utf-8', {
fatal: true,
ignoreBOM: true,
});

cachedTextDecoder.decode();

let cachedUint8Memory0 = new Uint8Array();

function getUint8Memory0() {
if (cachedUint8Memory0.byteLength === 0) {
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8Memory0;
}

function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}

function logError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
const error = (function () {
try {
return e instanceof Error
? `${e.message}\n\nStack:\n${e.stack}`
: e.toString();
} catch (_) {
return '<failed to stringify thrown value>';
}
})();
console.error(
'wasm-bindgen: imported JS function that was not marked as `catch` threw an error:',
error,
);
throw e;
}
}

let WASM_VECTOR_LEN = 0;

const lTextEncoder =
typeof TextEncoder === 'undefined'
? (0, module.require)('util').TextEncoder
: TextEncoder;

const cachedTextEncoder = new lTextEncoder('utf-8');

const encodeString =
typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length,
};
};

function passStringToWasm0(arg, malloc, realloc) {
if (typeof arg !== 'string') throw new Error('expected a string argument');

if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length);
getUint8Memory0()
.subarray(ptr, ptr + buf.length)
.set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}

let len = arg.length;
let ptr = malloc(len);

const mem = getUint8Memory0();

let offset = 0;

for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7f) break;
mem[ptr + offset] = code;
}

if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, (len = offset + arg.length * 3));
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
if (ret.read !== arg.length) throw new Error('failed to pass whole string');
offset += ret.written;
}

WASM_VECTOR_LEN = offset;
return ptr;
}
/**
* @param {string} name
*/
export function greet(name) {
const ptr0 = passStringToWasm0(
name,
wasm.__wbindgen_malloc,
wasm.__wbindgen_realloc,
);
const len0 = WASM_VECTOR_LEN;
wasm.greet(ptr0, len0);
}

export function __wbg_alert_9ea5a791b0d4c7a3() {
return logError((arg0, arg1) => {
// eslint-disable-next-line no-undef
alert(getStringFromWasm0(arg0, arg1));
}, arguments);
}

export function __wbindgen_throw(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
}
Binary file added e2e/native-esm/wasm-bindgen/index_bg.wasm
Binary file not shown.
8 changes: 6 additions & 2 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -1715,9 +1715,13 @@ export default class Runtime {
const moduleLookup: Record<string, VMModule> = {};
for (const {module} of imports) {
if (moduleLookup[module] === undefined) {
moduleLookup[module] = await this.loadEsmModule(
await this.resolveModule(module, identifier, context),
const resolvedModule = await this.resolveModule(
module,
identifier,
context,
);

moduleLookup[module] = await this.linkAndEvaluateModule(resolvedModule);
}
}

Expand Down