diff --git a/CHANGELOG.md b/CHANGELOG.md index f89c480e19c8..f32db97e7b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 9d3fd2c87ec8..e711c1a45c02 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -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: <> Ran all test suites matching /native-esm-wasm.test.js/i." diff --git a/e2e/native-esm/__tests__/native-esm-wasm.test.js b/e2e/native-esm/__tests__/native-esm-wasm.test.js index a48d6628eeff..fddc1d2f08f9 100644 --- a/e2e/native-esm/__tests__/native-esm-wasm.test.js +++ b/e2e/native-esm/__tests__/native-esm-wasm.test.js @@ -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'; @@ -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!'); +}); diff --git a/e2e/native-esm/wasm-bindgen/index.js b/e2e/native-esm/wasm-bindgen/index.js new file mode 100644 index 000000000000..1eec684e23fb --- /dev/null +++ b/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'; +export * from './index_bg.js'; diff --git a/e2e/native-esm/wasm-bindgen/index_bg.js b/e2e/native-esm/wasm-bindgen/index_bg.js new file mode 100644 index 000000000000..a194a2f5fb2c --- /dev/null +++ b/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 ''; + } + })(); + 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)); +} diff --git a/e2e/native-esm/wasm-bindgen/index_bg.wasm b/e2e/native-esm/wasm-bindgen/index_bg.wasm new file mode 100644 index 000000000000..e545fdfd14d3 Binary files /dev/null and b/e2e/native-esm/wasm-bindgen/index_bg.wasm differ diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index b9250afcd3bd..21bf82700fdf 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1715,9 +1715,13 @@ export default class Runtime { const moduleLookup: Record = {}; 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); } }