From 95ef9766aafd9a01a4977e87bb817174c668f7bf Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Tue, 25 Oct 2022 00:32:57 +0100 Subject: [PATCH 01/23] Add failing test --- e2e/__tests__/nativeEsmWasm.test.ts | 23 +++++++++++++++++++ e2e/native-esm-wasm/__tests__/answer.test.js | 12 ++++++++++ e2e/native-esm-wasm/answer.js | 13 +++++++++++ e2e/native-esm-wasm/answer.wasm | Bin 0 -> 181 bytes e2e/native-esm-wasm/package.json | 8 +++++++ 5 files changed, 56 insertions(+) create mode 100644 e2e/__tests__/nativeEsmWasm.test.ts create mode 100644 e2e/native-esm-wasm/__tests__/answer.test.js create mode 100644 e2e/native-esm-wasm/answer.js create mode 100644 e2e/native-esm-wasm/answer.wasm create mode 100644 e2e/native-esm-wasm/package.json diff --git a/e2e/__tests__/nativeEsmWasm.test.ts b/e2e/__tests__/nativeEsmWasm.test.ts new file mode 100644 index 000000000000..0d286cc8753b --- /dev/null +++ b/e2e/__tests__/nativeEsmWasm.test.ts @@ -0,0 +1,23 @@ +/** + * 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 {resolve} from 'path'; +import {json as runJest} from '../runJest'; + +const DIR = resolve(__dirname, '../native-esm-wasm'); + +test('runs WASM test with native ESM', () => { + const {exitCode, json} = runJest(DIR, [], { + nodeOptions: + '--experimental-vm-modules --experimental-wasm-modules --no-warnings', + }); + + expect(exitCode).toBe(0); + + expect(json.numTotalTests).toBe(1); + expect(json.numPassedTests).toBe(1); +}); diff --git a/e2e/native-esm-wasm/__tests__/answer.test.js b/e2e/native-esm-wasm/__tests__/answer.test.js new file mode 100644 index 000000000000..c6e3cff1ae28 --- /dev/null +++ b/e2e/native-esm-wasm/__tests__/answer.test.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. + */ + +import {getAnswer} from '../answer'; + +test('test answer', () => { + expect(getAnswer()).toBe(42); +}); diff --git a/e2e/native-esm-wasm/answer.js b/e2e/native-esm-wasm/answer.js new file mode 100644 index 000000000000..0fc6d243cb8a --- /dev/null +++ b/e2e/native-esm-wasm/answer.js @@ -0,0 +1,13 @@ +/** + * 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 './answer.wasm'; + +export function getAnswer() { + const ret = wasm.getAnswer(); + return ret; +} diff --git a/e2e/native-esm-wasm/answer.wasm b/e2e/native-esm-wasm/answer.wasm new file mode 100644 index 0000000000000000000000000000000000000000..2dd107d7b789375eab071e611c0cae746f3b5c84 GIT binary patch literal 181 zcmWNLy$*sf6h?2$kD@L*xS2452~7(IVdNEj0t;M;2^7;p2yyV$ZSqa$oZ$m$1_A)Y zBTpbdhZ67{0>{mn`U4K5UMU0}PL(nXYJi41VmqO+!?_tW*G~tC^>@Tvnna`Oh89yxcRZ_oI+o3vf Qma-xXBdld!idR;|f3hts^Z)<= literal 0 HcmV?d00001 diff --git a/e2e/native-esm-wasm/package.json b/e2e/native-esm-wasm/package.json new file mode 100644 index 000000000000..3457bb63f174 --- /dev/null +++ b/e2e/native-esm-wasm/package.json @@ -0,0 +1,8 @@ +{ + "type": "module", + "name": "native-esm-wasm", + "jest": { + "testEnvironment": "node", + "transform": {} + } +} From 645db88d64cd3ccb2ab053b955c0ca2cbe50f5bc Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Tue, 25 Oct 2022 00:34:25 +0100 Subject: [PATCH 02/23] Update `jest-runtime` to fix tests --- e2e/native-esm/__tests__/native-esm.test.js | 2 +- packages/jest-runtime/src/index.ts | 63 +++++++++++++++++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/e2e/native-esm/__tests__/native-esm.test.js b/e2e/native-esm/__tests__/native-esm.test.js index 6403e741ac65..befe571a01d3 100644 --- a/e2e/native-esm/__tests__/native-esm.test.js +++ b/e2e/native-esm/__tests__/native-esm.test.js @@ -258,7 +258,7 @@ test('imports from "data:text/javascript" URI with invalid data fail', async () test('imports from "data:application/wasm" URI not supported', async () => { await expect(() => import('data:application/wasm,96cafe00babe'), - ).rejects.toThrow('WASM is currently not supported'); + ).rejects.toThrow('WASM in data URLs is currently not supported'); }); test('supports imports from "data:application/json" URI', async () => { diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 7be6cf2db53d..20d35f6df9e7 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -398,9 +398,12 @@ export default class Runtime { // unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it unstable_shouldLoadAsEsm(path: string): boolean { - return Resolver.unstable_shouldLoadAsEsm( - path, - this._config.extensionsToTreatAsEsm, + return ( + path.endsWith('wasm') || + Resolver.unstable_shouldLoadAsEsm( + path, + this._config.extensionsToTreatAsEsm, + ) ); } @@ -441,6 +444,15 @@ export default class Runtime { 'Promise initialization should be sync - please report this bug to Jest!', ); + if (modulePath.endsWith('wasm')) { + const wasm = this._importWasmModule(modulePath, context); + + this._esmoduleRegistry.set(cacheKey, wasm); + + transformResolve(); + return wasm; + } + if (this._resolver.isCoreModule(modulePath)) { const core = this._importCoreModule(modulePath, context); this._esmoduleRegistry.set(cacheKey, core); @@ -568,7 +580,7 @@ export default class Runtime { const mime = match.groups.mime; if (mime === 'application/wasm') { - throw new Error('WASM is currently not supported'); + throw new Error('WASM in data URLs is currently not supported'); } const encoding = match.groups.encoding; @@ -1640,6 +1652,49 @@ export default class Runtime { return evaluateSyntheticModule(module); } + private async _importWasmModule(moduleName: string, context: VMContext) { + const wasmModule = await WebAssembly.compile(fs.readFileSync(moduleName)); + + const exports = WebAssembly.Module.exports(wasmModule); + const imports = WebAssembly.Module.imports(wasmModule); + + const moduleLookup: Record = {}; + for (const {module} of imports) { + if (moduleLookup[module] === undefined) { + moduleLookup[module] = await this.linkAndEvaluateModule( + await this.resolveModule(module, moduleName, context), + ); + } + } + + const syntheticModule = new SyntheticModule( + exports.map(({name}) => name), + function () { + const importsObject: WebAssembly.Imports = {}; + for (const {module, name} of imports) { + if (!importsObject[module]) { + importsObject[module] = {}; + } + importsObject[module][name] = moduleLookup[module].namespace[name]; + } + const wasmInstance = new WebAssembly.Instance( + wasmModule, + importsObject, + ); + for (const {name} of exports) { + // @ts-expect-error: TS doesn't know what `this` is + this.setExport(name, wasmInstance.exports[name]); + } + }, + { + context, + identifier: moduleName, + }, + ); + + return syntheticModule; + } + private _getMockedNativeModule(): typeof nativeModule.Module { if (this._moduleImplementation) { return this._moduleImplementation; From 99851798a9f4429592dcba7ffe13d27444c611f7 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Thu, 27 Oct 2022 22:43:06 +0100 Subject: [PATCH 03/23] Support data:application/wasm imports --- .../__snapshots__/nativeEsm.test.ts.snap | 2 +- e2e/__tests__/nativeEsmWasm.test.ts | 4 +- e2e/native-esm-wasm/__tests__/answer.test.js | 29 ++++- e2e/native-esm-wasm/answer.js | 13 -- e2e/native-esm/__tests__/native-esm.test.js | 6 - packages/jest-runtime/src/index.ts | 122 ++++++++++-------- 6 files changed, 99 insertions(+), 77 deletions(-) delete mode 100644 e2e/native-esm-wasm/answer.js diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 4324b26d16c4..1e6cd80fa82e 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 test with native ESM 1`] = ` "Test Suites: 1 passed, 1 total -Tests: 34 passed, 34 total +Tests: 33 passed, 33 total Snapshots: 0 total Time: <> Ran all test suites matching /native-esm.test.js/i." diff --git a/e2e/__tests__/nativeEsmWasm.test.ts b/e2e/__tests__/nativeEsmWasm.test.ts index 0d286cc8753b..e19e4454fe70 100644 --- a/e2e/__tests__/nativeEsmWasm.test.ts +++ b/e2e/__tests__/nativeEsmWasm.test.ts @@ -18,6 +18,6 @@ test('runs WASM test with native ESM', () => { expect(exitCode).toBe(0); - expect(json.numTotalTests).toBe(1); - expect(json.numPassedTests).toBe(1); + expect(json.numTotalTests).toBe(4); + expect(json.numPassedTests).toBe(4); }); diff --git a/e2e/native-esm-wasm/__tests__/answer.test.js b/e2e/native-esm-wasm/__tests__/answer.test.js index c6e3cff1ae28..5773887e3b5a 100644 --- a/e2e/native-esm-wasm/__tests__/answer.test.js +++ b/e2e/native-esm-wasm/__tests__/answer.test.js @@ -5,8 +5,33 @@ * LICENSE file in the root directory of this source tree. */ -import {getAnswer} from '../answer'; +// the point here is that it's the node core module +// eslint-disable-next-line no-restricted-imports +import {readFileSync} from 'fs'; +// The file was generated by wasm-pack +import {getAnswer} from '../answer.wasm'; -test('test answer', () => { +const wasmFileBuffer = readFileSync('answer.wasm'); + +test('supports native wasm imports', () => { expect(getAnswer()).toBe(42); }); + +test('supports imports from "data:application/wasm" URI with base64 encoding', async () => { + const importedWasmModule = await import( + `data:application/wasm;base64,${wasmFileBuffer.toString('base64')}` + ); + expect(importedWasmModule.getAnswer()).toBe(42); +}); + +test('imports from "data:text/wasm" URI without explicit encoding fail', async () => { + await expect(() => + import(`data:application/wasm,${wasmFileBuffer.toString('base64')}`), + ).rejects.toThrow('Missing data URI encoding'); +}); + +test('imports from "data:text/wasm" URI with invalid encoding fail', async () => { + await expect(() => + import(`data:application/wasm,hex,${wasmFileBuffer.toString('hex')}`), + ).rejects.toThrow('Missing data URI encoding'); +}); diff --git a/e2e/native-esm-wasm/answer.js b/e2e/native-esm-wasm/answer.js deleted file mode 100644 index 0fc6d243cb8a..000000000000 --- a/e2e/native-esm-wasm/answer.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * 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 './answer.wasm'; - -export function getAnswer() { - const ret = wasm.getAnswer(); - return ret; -} diff --git a/e2e/native-esm/__tests__/native-esm.test.js b/e2e/native-esm/__tests__/native-esm.test.js index befe571a01d3..4b253fe6bca1 100644 --- a/e2e/native-esm/__tests__/native-esm.test.js +++ b/e2e/native-esm/__tests__/native-esm.test.js @@ -255,12 +255,6 @@ test('imports from "data:text/javascript" URI with invalid data fail', async () ).rejects.toThrow("Unexpected token '.'"); }); -test('imports from "data:application/wasm" URI not supported', async () => { - await expect(() => - import('data:application/wasm,96cafe00babe'), - ).rejects.toThrow('WASM in data URLs is currently not supported'); -}); - test('supports imports from "data:application/json" URI', async () => { const data = await import('data:application/json,{"foo": "bar"}'); expect(data.default).toEqual({foo: 'bar'}); diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 20d35f6df9e7..195149a07941 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -445,7 +445,11 @@ export default class Runtime { ); if (modulePath.endsWith('wasm')) { - const wasm = this._importWasmModule(modulePath, context); + const wasm = this._importWasmModule( + fs.readFileSync(modulePath), + modulePath, + context, + ); this._esmoduleRegistry.set(cacheKey, wasm); @@ -579,56 +583,67 @@ export default class Runtime { } const mime = match.groups.mime; - if (mime === 'application/wasm') { - throw new Error('WASM in data URLs is currently not supported'); - } - const encoding = match.groups.encoding; - let code = match.groups.code; - if (!encoding || encoding === 'charset=utf-8') { - code = decodeURIComponent(code); - } else if (encoding === 'base64') { - code = Buffer.from(code, 'base64').toString(); - } else { - throw new Error(`Invalid data URI encoding: ${encoding}`); - } - let module; - if (mime === 'application/json') { - module = new SyntheticModule( - ['default'], - function () { - const obj = JSON.parse(code); - // @ts-expect-error: TS doesn't know what `this` is - this.setExport('default', obj); - }, - {context, identifier: specifier}, + + if (mime === 'application/wasm') { + if (!encoding) { + throw new Error('Missing data URI encoding'); + } + if (encoding !== 'base64') { + throw new Error(`Invalid data URI encoding: ${encoding}`); + } + module = await this._importWasmModule( + Buffer.from(match.groups.code, 'base64'), + specifier, + context, ); } else { - module = new SourceTextModule(code, { - context, - identifier: specifier, - importModuleDynamically: async ( - specifier: string, - referencingModule: VMModule, - ) => { - invariant( - runtimeSupportsVmModules, - 'You need to run with a version of node that supports ES Modules in the VM API. See https://jestjs.io/docs/ecmascript-modules', - ); - const module = await this.resolveModule( - specifier, - referencingModule.identifier, - referencingModule.context, - ); + let code = match.groups.code; + if (!encoding || encoding === 'charset=utf-8') { + code = decodeURIComponent(code); + } else if (encoding === 'base64') { + code = Buffer.from(code, 'base64').toString(); + } else { + throw new Error(`Invalid data URI encoding: ${encoding}`); + } - return this.linkAndEvaluateModule(module); - }, - initializeImportMeta(meta: ImportMeta) { - // no `jest` here as it's not loaded in a file - meta.url = specifier; - }, - }); + if (mime === 'application/json') { + module = new SyntheticModule( + ['default'], + function () { + const obj = JSON.parse(code); + // @ts-expect-error: TS doesn't know what `this` is + this.setExport('default', obj); + }, + {context, identifier: specifier}, + ); + } else { + module = new SourceTextModule(code, { + context, + identifier: specifier, + importModuleDynamically: async ( + specifier: string, + referencingModule: VMModule, + ) => { + invariant( + runtimeSupportsVmModules, + 'You need to run with a version of node that supports ES Modules in the VM API. See https://jestjs.io/docs/ecmascript-modules', + ); + const module = await this.resolveModule( + specifier, + referencingModule.identifier, + referencingModule.context, + ); + + return this.linkAndEvaluateModule(module); + }, + initializeImportMeta(meta: ImportMeta) { + // no `jest` here as it's not loaded in a file + meta.url = specifier; + }, + }); + } } this._esmoduleRegistry.set(specifier, module); @@ -1652,8 +1667,12 @@ export default class Runtime { return evaluateSyntheticModule(module); } - private async _importWasmModule(moduleName: string, context: VMContext) { - const wasmModule = await WebAssembly.compile(fs.readFileSync(moduleName)); + private async _importWasmModule( + source: Buffer, + identifier: string, + context: VMContext, + ) { + const wasmModule = await WebAssembly.compile(source); const exports = WebAssembly.Module.exports(wasmModule); const imports = WebAssembly.Module.imports(wasmModule); @@ -1662,7 +1681,7 @@ export default class Runtime { for (const {module} of imports) { if (moduleLookup[module] === undefined) { moduleLookup[module] = await this.linkAndEvaluateModule( - await this.resolveModule(module, moduleName, context), + await this.resolveModule(module, identifier, context), ); } } @@ -1686,10 +1705,7 @@ export default class Runtime { this.setExport(name, wasmInstance.exports[name]); } }, - { - context, - identifier: moduleName, - }, + {context, identifier}, ); return syntheticModule; From 5d73f9a63b65dadb002c9bb28495298661b7ed8f Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Thu, 27 Oct 2022 22:45:20 +0100 Subject: [PATCH 04/23] Run tests twice (with and without --experimental-wasm-modules) --- e2e/__tests__/nativeEsmWasm.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/e2e/__tests__/nativeEsmWasm.test.ts b/e2e/__tests__/nativeEsmWasm.test.ts index e19e4454fe70..4b5c2ad313ec 100644 --- a/e2e/__tests__/nativeEsmWasm.test.ts +++ b/e2e/__tests__/nativeEsmWasm.test.ts @@ -11,6 +11,17 @@ import {json as runJest} from '../runJest'; const DIR = resolve(__dirname, '../native-esm-wasm'); test('runs WASM test with native ESM', () => { + const {exitCode, json} = runJest(DIR, [], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); + + expect(exitCode).toBe(0); + + expect(json.numTotalTests).toBe(4); + expect(json.numPassedTests).toBe(4); +}); + +test('runs WASM test with native ESM with --experimental-wasm-modules flag', () => { const {exitCode, json} = runJest(DIR, [], { nodeOptions: '--experimental-vm-modules --experimental-wasm-modules --no-warnings', From 734bbc5448d7dac58f8fc6459abe54fd729d6ffe Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Thu, 27 Oct 2022 22:51:47 +0100 Subject: [PATCH 05/23] Cover more rows --- e2e/native-esm-wasm/__tests__/answer.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/native-esm-wasm/__tests__/answer.test.js b/e2e/native-esm-wasm/__tests__/answer.test.js index 5773887e3b5a..5d3771c7c8b5 100644 --- a/e2e/native-esm-wasm/__tests__/answer.test.js +++ b/e2e/native-esm-wasm/__tests__/answer.test.js @@ -32,6 +32,6 @@ test('imports from "data:text/wasm" URI without explicit encoding fail', async ( test('imports from "data:text/wasm" URI with invalid encoding fail', async () => { await expect(() => - import(`data:application/wasm,hex,${wasmFileBuffer.toString('hex')}`), - ).rejects.toThrow('Missing data URI encoding'); + import('data:application/wasm;charset=utf-8,oops'), + ).rejects.toThrow('Invalid data URI encoding: charset=utf-8'); }); From 612d480b9147eed811246058731f7706d01c9814 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Thu, 27 Oct 2022 23:03:10 +0100 Subject: [PATCH 06/23] Add CHANGELOG entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 494e36427afb..1721be9a79ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[jest-runtime]` Support WASM imports ([#13505](https://github.com/facebook/jest/pull/13505)) + ### Fixes - `[jest-mock]` Treat cjs modules as objects so they can be mocked ([#13513](https://github.com/facebook/jest/pull/13513)) From dcff11e50e14934e548c2b08e8b45cb5e91968dd Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Thu, 27 Oct 2022 23:16:19 +0100 Subject: [PATCH 07/23] Simplify tests --- .../__snapshots__/nativeEsm.test.ts.snap | 8 +++++ e2e/__tests__/nativeEsm.test.ts | 12 +++++++ e2e/__tests__/nativeEsmWasm.test.ts | 34 ------------------ .../answer.wasm => native-esm/42.wasm} | Bin .../__tests__/native-esm-wasm.test.js} | 4 +-- 5 files changed, 22 insertions(+), 36 deletions(-) delete mode 100644 e2e/__tests__/nativeEsmWasm.test.ts rename e2e/{native-esm-wasm/answer.wasm => native-esm/42.wasm} (100%) rename e2e/{native-esm-wasm/__tests__/answer.test.js => native-esm/__tests__/native-esm-wasm.test.js} (92%) diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 1e6cd80fa82e..648efbfd14c9 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -8,6 +8,14 @@ Time: <> Ran all test suites matching /native-esm-deep-cjs-reexport.test.js/i." `; +exports[`runs WASM test with native ESM 1`] = ` +"Test Suites: 1 passed, 1 total +Tests: 4 passed, 4 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /native-esm-wasm.test.js/i." +`; + exports[`runs test with native ESM 1`] = ` "Test Suites: 1 passed, 1 total Tests: 33 passed, 33 total diff --git a/e2e/__tests__/nativeEsm.test.ts b/e2e/__tests__/nativeEsm.test.ts index a636fdf1344a..f5227089c623 100644 --- a/e2e/__tests__/nativeEsm.test.ts +++ b/e2e/__tests__/nativeEsm.test.ts @@ -67,3 +67,15 @@ onNodeVersions('>=16.9.0', () => { expect(exitCode).toBe(0); }); }); + +test('runs WASM test with native ESM', () => { + const {exitCode, stderr, stdout} = runJest(DIR, ['native-esm-wasm.test.js'], { + nodeOptions: '--experimental-vm-modules --no-warnings', + }); + + const {summary} = extractSummary(stderr); + + expect(summary).toMatchSnapshot(); + expect(stdout).toBe(''); + expect(exitCode).toBe(0); +}); diff --git a/e2e/__tests__/nativeEsmWasm.test.ts b/e2e/__tests__/nativeEsmWasm.test.ts deleted file mode 100644 index 4b5c2ad313ec..000000000000 --- a/e2e/__tests__/nativeEsmWasm.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 {resolve} from 'path'; -import {json as runJest} from '../runJest'; - -const DIR = resolve(__dirname, '../native-esm-wasm'); - -test('runs WASM test with native ESM', () => { - const {exitCode, json} = runJest(DIR, [], { - nodeOptions: '--experimental-vm-modules --no-warnings', - }); - - expect(exitCode).toBe(0); - - expect(json.numTotalTests).toBe(4); - expect(json.numPassedTests).toBe(4); -}); - -test('runs WASM test with native ESM with --experimental-wasm-modules flag', () => { - const {exitCode, json} = runJest(DIR, [], { - nodeOptions: - '--experimental-vm-modules --experimental-wasm-modules --no-warnings', - }); - - expect(exitCode).toBe(0); - - expect(json.numTotalTests).toBe(4); - expect(json.numPassedTests).toBe(4); -}); diff --git a/e2e/native-esm-wasm/answer.wasm b/e2e/native-esm/42.wasm similarity index 100% rename from e2e/native-esm-wasm/answer.wasm rename to e2e/native-esm/42.wasm diff --git a/e2e/native-esm-wasm/__tests__/answer.test.js b/e2e/native-esm/__tests__/native-esm-wasm.test.js similarity index 92% rename from e2e/native-esm-wasm/__tests__/answer.test.js rename to e2e/native-esm/__tests__/native-esm-wasm.test.js index 5d3771c7c8b5..17d4f4700823 100644 --- a/e2e/native-esm-wasm/__tests__/answer.test.js +++ b/e2e/native-esm/__tests__/native-esm-wasm.test.js @@ -9,9 +9,9 @@ // eslint-disable-next-line no-restricted-imports import {readFileSync} from 'fs'; // The file was generated by wasm-pack -import {getAnswer} from '../answer.wasm'; +import {getAnswer} from '../42.wasm'; -const wasmFileBuffer = readFileSync('answer.wasm'); +const wasmFileBuffer = readFileSync('42.wasm'); test('supports native wasm imports', () => { expect(getAnswer()).toBe(42); From e524e409aa1aca35fe404f1793b88f44110640be Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Thu, 27 Oct 2022 23:23:45 +0100 Subject: [PATCH 08/23] Improve `path.endsWith` --- packages/jest-runtime/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 195149a07941..284be823c23b 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -399,7 +399,7 @@ export default class Runtime { // unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it unstable_shouldLoadAsEsm(path: string): boolean { return ( - path.endsWith('wasm') || + path.endsWith('.wasm') || Resolver.unstable_shouldLoadAsEsm( path, this._config.extensionsToTreatAsEsm, @@ -444,7 +444,7 @@ export default class Runtime { 'Promise initialization should be sync - please report this bug to Jest!', ); - if (modulePath.endsWith('wasm')) { + if (modulePath.endsWith('.wasm')) { const wasm = this._importWasmModule( fs.readFileSync(modulePath), modulePath, From 08c890602871ae84962c89190bab48abe0a721fd Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Thu, 27 Oct 2022 23:24:39 +0100 Subject: [PATCH 09/23] Delete unused file --- e2e/native-esm-wasm/package.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 e2e/native-esm-wasm/package.json diff --git a/e2e/native-esm-wasm/package.json b/e2e/native-esm-wasm/package.json deleted file mode 100644 index 3457bb63f174..000000000000 --- a/e2e/native-esm-wasm/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "module", - "name": "native-esm-wasm", - "jest": { - "testEnvironment": "node", - "transform": {} - } -} From 66c8312df60b2ab96b22cbd94629285fdbac08d6 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Fri, 28 Oct 2022 00:06:01 +0100 Subject: [PATCH 10/23] Make changelog phrase more explicit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1721be9a79ae..1411ae431a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- `[jest-runtime]` Support WASM imports ([#13505](https://github.com/facebook/jest/pull/13505)) +- `[jest-runtime]` Support WASM imports in ESM modules ([#13505](https://github.com/facebook/jest/pull/13505)) ### Fixes From 6586a24c9016ef3073ac92acd2b3f9c312c40c5d Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Fri, 28 Oct 2022 00:11:46 +0100 Subject: [PATCH 11/23] Fix Wasm casing --- CHANGELOG.md | 2 +- e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap | 2 +- e2e/__tests__/nativeEsm.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1411ae431a2a..66f632af4cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- `[jest-runtime]` Support WASM imports in ESM modules ([#13505](https://github.com/facebook/jest/pull/13505)) +- `[jest-runtime]` Support Wasm imports in ESM modules ([#13505](https://github.com/facebook/jest/pull/13505)) ### Fixes diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 648efbfd14c9..a9d283f5e7e1 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -8,7 +8,7 @@ Time: <> Ran all test suites matching /native-esm-deep-cjs-reexport.test.js/i." `; -exports[`runs WASM test with native ESM 1`] = ` +exports[`runs Wasm test with native ESM 1`] = ` "Test Suites: 1 passed, 1 total Tests: 4 passed, 4 total Snapshots: 0 total diff --git a/e2e/__tests__/nativeEsm.test.ts b/e2e/__tests__/nativeEsm.test.ts index f5227089c623..dd0078af63c7 100644 --- a/e2e/__tests__/nativeEsm.test.ts +++ b/e2e/__tests__/nativeEsm.test.ts @@ -68,7 +68,7 @@ onNodeVersions('>=16.9.0', () => { }); }); -test('runs WASM test with native ESM', () => { +test('runs Wasm test with native ESM', () => { const {exitCode, stderr, stdout} = runJest(DIR, ['native-esm-wasm.test.js'], { nodeOptions: '--experimental-vm-modules --no-warnings', }); From 01f8d78decbd6756925d15980a5be5a7a46d2e1a Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Sun, 30 Oct 2022 17:07:42 +0000 Subject: [PATCH 12/23] Update CHANGELOG.md Co-authored-by: Simen Bekkhus --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f632af4cb3..c46276c9a822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- `[jest-runtime]` Support Wasm imports in ESM modules ([#13505](https://github.com/facebook/jest/pull/13505)) +- `[jest-runtime]` Support WebAssembly (Wasm) imports in ESM modules ([#13505](https://github.com/facebook/jest/pull/13505)) ### Fixes From 27d73b67eae02d27b94d9b6b62b8733ad9cf371e Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Sun, 30 Oct 2022 17:19:01 +0000 Subject: [PATCH 13/23] Mention Wasm in docs --- docs/ECMAScriptModules.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ECMAScriptModules.md b/docs/ECMAScriptModules.md index e1ad627556a1..047eeabe576c 100644 --- a/docs/ECMAScriptModules.md +++ b/docs/ECMAScriptModules.md @@ -22,6 +22,8 @@ With the warnings out of the way, this is how you activate ESM support in your t If you use Yarn, you can use `yarn node --experimental-vm-modules $(yarn bin jest)`. This command will also work if you use [Yarn Plug'n'Play](https://yarnpkg.com/features/pnp). + If your codebase includes ESM imports from `*.wasm` files, you do _not_ need to pass `--experimental-wasm-modules` to `node`. Current implementation of WebAssembly imports in Jest relies on experimental VM modules, however, this may change in the future. + 1. Beyond that, we attempt to follow `node`'s logic for activating "ESM mode" (such as looking at `type` in `package.json` or `.mjs` files), see [their docs](https://nodejs.org/api/esm.html#esm_enabling) for details. 1. If you want to treat other file extensions (such as `.jsx` or `.ts`) as ESM, please use the [`extensionsToTreatAsEsm` option](Configuration.md#extensionstotreatasesm-arraystring). From 79e0f40d9397acb923595aa7c9070c24260f4274 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Sun, 30 Oct 2022 18:23:17 +0000 Subject: [PATCH 14/23] Use Wasm file from mdn examples --- .../__snapshots__/nativeEsm.test.ts.snap | 2 +- e2e/__tests__/nativeEsm.test.ts | 2 +- e2e/native-esm/42.wasm | Bin 181 -> 0 bytes .../__tests__/native-esm-wasm.test.js | 26 ++++++++++++++---- e2e/native-esm/add.wasm | Bin 0 -> 41 bytes 5 files changed, 23 insertions(+), 7 deletions(-) delete mode 100644 e2e/native-esm/42.wasm create mode 100644 e2e/native-esm/add.wasm diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index a9d283f5e7e1..950d7c45945c 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -8,7 +8,7 @@ Time: <> Ran all test suites matching /native-esm-deep-cjs-reexport.test.js/i." `; -exports[`runs Wasm test with native ESM 1`] = ` +exports[`runs WebAssembly (Wasm) test with native ESM 1`] = ` "Test Suites: 1 passed, 1 total Tests: 4 passed, 4 total Snapshots: 0 total diff --git a/e2e/__tests__/nativeEsm.test.ts b/e2e/__tests__/nativeEsm.test.ts index dd0078af63c7..a25d2dbd11c7 100644 --- a/e2e/__tests__/nativeEsm.test.ts +++ b/e2e/__tests__/nativeEsm.test.ts @@ -68,7 +68,7 @@ onNodeVersions('>=16.9.0', () => { }); }); -test('runs Wasm test with native ESM', () => { +test('runs WebAssembly (Wasm) test with native ESM', () => { const {exitCode, stderr, stdout} = runJest(DIR, ['native-esm-wasm.test.js'], { nodeOptions: '--experimental-vm-modules --no-warnings', }); diff --git a/e2e/native-esm/42.wasm b/e2e/native-esm/42.wasm deleted file mode 100644 index 2dd107d7b789375eab071e611c0cae746f3b5c84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181 zcmWNLy$*sf6h?2$kD@L*xS2452~7(IVdNEj0t;M;2^7;p2yyV$ZSqa$oZ$m$1_A)Y zBTpbdhZ67{0>{mn`U4K5UMU0}PL(nXYJi41VmqO+!?_tW*G~tC^>@Tvnna`Oh89yxcRZ_oI+o3vf Qma-xXBdld!idR;|f3hts^Z)<= diff --git a/e2e/native-esm/__tests__/native-esm-wasm.test.js b/e2e/native-esm/__tests__/native-esm-wasm.test.js index 17d4f4700823..2755c5e9805b 100644 --- a/e2e/native-esm/__tests__/native-esm-wasm.test.js +++ b/e2e/native-esm/__tests__/native-esm-wasm.test.js @@ -8,20 +8,36 @@ // the point here is that it's the node core module // eslint-disable-next-line no-restricted-imports import {readFileSync} from 'fs'; -// The file was generated by wasm-pack -import {getAnswer} from '../42.wasm'; +// 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'; -const wasmFileBuffer = readFileSync('42.wasm'); +const wasmFileBuffer = readFileSync('add.wasm'); test('supports native wasm imports', () => { - expect(getAnswer()).toBe(42); + expect(add(1, 2)).toBe(3); + + // because arguments are i32 (signed), fractional part is truncated + expect(add(0.99, 1.01)).toBe(1); + + // because return value is i32 (signed), (2^31 - 1) + 1 overflows and becomes -2^31 + expect(add(Math.pow(2, 31) - 1, 1)).toBe(-Math.pow(2, 31)); + + // invalid or missing arguments are treated as 0 + expect(add('hello', 'world')).toBe(0); + expect(add()).toBe(0); + expect(add(null)).toBe(0); + expect(add({}, [])).toBe(0); + + // redundant arguments are silently ignored + expect(add(1, 2, 3)).toBe(3); }); test('supports imports from "data:application/wasm" URI with base64 encoding', async () => { const importedWasmModule = await import( `data:application/wasm;base64,${wasmFileBuffer.toString('base64')}` ); - expect(importedWasmModule.getAnswer()).toBe(42); + expect(importedWasmModule.add(0, 42)).toBe(42); }); test('imports from "data:text/wasm" URI without explicit encoding fail', async () => { diff --git a/e2e/native-esm/add.wasm b/e2e/native-esm/add.wasm new file mode 100644 index 0000000000000000000000000000000000000000..357f72da7a0db8add83699082fd51d46bf3352fb GIT binary patch literal 41 wcmZQbEY4+QU|?WmXG~zKuV<`hW@2PuXJ=$iOi5v2;NoOtXHZ~JV9eqM0DJxgJ^%m! literal 0 HcmV?d00001 From 0d4945e45bf3297ffd190bf89c9aa41083025067 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Sun, 30 Oct 2022 18:33:42 +0000 Subject: [PATCH 15/23] Implement `readFileBuffer` --- docs/CodeTransformation.md | 2 +- packages/babel-jest/src/__tests__/index.ts | 8 +++---- packages/jest-repl/src/cli/repl.ts | 2 +- packages/jest-runtime/src/index.ts | 28 +++++++++++++++++++--- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/docs/CodeTransformation.md b/docs/CodeTransformation.md index 2d16208eb10f..3be3e0ac9a0e 100644 --- a/docs/CodeTransformation.md +++ b/docs/CodeTransformation.md @@ -40,7 +40,7 @@ interface TransformOptions { supportsTopLevelAwait: boolean; instrument: boolean; /** Cached file system which is used by `jest-runtime` to improve performance. */ - cacheFS: Map; + cacheFS: Map; /** Jest configuration of currently running project. */ config: ProjectConfig; /** Stringified version of the `config` - useful in cache busting. */ diff --git a/packages/babel-jest/src/__tests__/index.ts b/packages/babel-jest/src/__tests__/index.ts index 9903c2b773b0..4808bd0e591a 100644 --- a/packages/babel-jest/src/__tests__/index.ts +++ b/packages/babel-jest/src/__tests__/index.ts @@ -52,7 +52,7 @@ test('Returns source string with inline maps when no transformOptions is passed' sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, @@ -76,7 +76,7 @@ test('Returns source string with inline maps when no transformOptions is passed sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, @@ -141,7 +141,7 @@ describe('caller option correctly merges from defaults and options', () => { ], ])('%j -> %j', (input, output) => { defaultBabelJestTransformer.process(sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, @@ -166,7 +166,7 @@ describe('caller option correctly merges from defaults and options', () => { test('can pass null to createTransformer', () => { const transformer = createTransformer(); transformer.process(sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, diff --git a/packages/jest-repl/src/cli/repl.ts b/packages/jest-repl/src/cli/repl.ts index 3a486c6bfbb7..aceb9f1fb180 100644 --- a/packages/jest-repl/src/cli/repl.ts +++ b/packages/jest-repl/src/cli/repl.ts @@ -33,7 +33,7 @@ const evalCommand: repl.REPLEval = ( cmd, jestGlobalConfig.replname ?? 'jest.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: jestProjectConfig, configString: JSON.stringify(jestProjectConfig), instrument: false, diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index ed0d5f9e50c2..5941ec3076db 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -153,7 +153,7 @@ const supportsNodeColonModulePrefixInRequire = (() => { })(); export default class Runtime { - private readonly _cacheFS: Map; + private readonly _cacheFS: Map; private readonly _config: Config.ProjectConfig; private readonly _globalConfig?: Config.GlobalConfig; private readonly _coverageOptions: ShouldInstrumentOptions; @@ -211,7 +211,7 @@ export default class Runtime { environment: JestEnvironment, resolver: Resolver, transformer: ScriptTransformer, - cacheFS: Map, + cacheFS: Map, coverageOptions: ShouldInstrumentOptions, testPath: string, // TODO: make mandatory in Jest 30 @@ -446,7 +446,7 @@ export default class Runtime { if (modulePath.endsWith('.wasm')) { const wasm = this._importWasmModule( - fs.readFileSync(modulePath), + this.readFileBuffer(modulePath), modulePath, context, ); @@ -2376,6 +2376,23 @@ export default class Runtime { }; } + private readFileBuffer(filename: string): Buffer { + let source = this._cacheFS.get(filename); + + if (!source) { + source = fs.readFileSync(filename); + + this._cacheFS.set(filename, source); + } + + if (typeof source === 'string') { + // This may occur if readFileBuffer and readFile are called interchangeably for one file + throw new Error("Expected source to be a 'Buffer', but got 'string'"); + } + + return source; + } + private readFile(filename: string): string { let source = this._cacheFS.get(filename); @@ -2385,6 +2402,11 @@ export default class Runtime { this._cacheFS.set(filename, source); } + if (typeof source !== 'string') { + // This may occur if readFileBuffer and readFile are called interchangeably for one file + throw new Error("Expected source to be a 'string', but got 'Buffer'"); + } + return source; } From 26db3fde3eb67fa82cd0ff8e7de19a74bdfff189 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Sun, 30 Oct 2022 18:58:52 +0000 Subject: [PATCH 16/23] Call loadEsmModule instead of linkAndEvaluateModule in _importWasmModule --- packages/jest-runtime/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 5941ec3076db..ee3e548941b8 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1680,7 +1680,7 @@ export default class Runtime { const moduleLookup: Record = {}; for (const {module} of imports) { if (moduleLookup[module] === undefined) { - moduleLookup[module] = await this.linkAndEvaluateModule( + moduleLookup[module] = await this.loadEsmModule( await this.resolveModule(module, identifier, context), ); } From 1e6d75bc83a41a48c5f0de42bf823a380029b076 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Mon, 31 Oct 2022 18:51:30 +0000 Subject: [PATCH 17/23] Update e2e/native-esm/__tests__/native-esm-wasm.test.js --- e2e/native-esm/__tests__/native-esm-wasm.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/native-esm/__tests__/native-esm-wasm.test.js b/e2e/native-esm/__tests__/native-esm-wasm.test.js index 2755c5e9805b..8caa5cf40993 100644 --- a/e2e/native-esm/__tests__/native-esm-wasm.test.js +++ b/e2e/native-esm/__tests__/native-esm-wasm.test.js @@ -40,13 +40,13 @@ test('supports imports from "data:application/wasm" URI with base64 encoding', a expect(importedWasmModule.add(0, 42)).toBe(42); }); -test('imports from "data:text/wasm" URI without explicit encoding fail', async () => { +test('imports from "data:application/wasm" URI without explicit encoding fail', async () => { await expect(() => import(`data:application/wasm,${wasmFileBuffer.toString('base64')}`), ).rejects.toThrow('Missing data URI encoding'); }); -test('imports from "data:text/wasm" URI with invalid encoding fail', async () => { +test('imports from "data:application/wasm" URI with invalid encoding fail', async () => { await expect(() => import('data:application/wasm;charset=utf-8,oops'), ).rejects.toThrow('Invalid data URI encoding: charset=utf-8'); From ad32edd04adecfb79f847fde95c1c12421307580 Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Tue, 1 Nov 2022 19:32:10 +0000 Subject: [PATCH 18/23] Add another test case for dynamic import --- e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap | 2 +- e2e/native-esm/__tests__/native-esm-wasm.test.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index 950d7c45945c..9d3fd2c87ec8 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: 4 passed, 4 total +Tests: 5 passed, 5 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 8caa5cf40993..853122085b4c 100644 --- a/e2e/native-esm/__tests__/native-esm-wasm.test.js +++ b/e2e/native-esm/__tests__/native-esm-wasm.test.js @@ -33,6 +33,11 @@ test('supports native wasm imports', () => { expect(add(1, 2, 3)).toBe(3); }); +test('supports dynamic wasm imports', async () => { + const {add: dynamicAdd} = await import('../add.wasm'); + expect(dynamicAdd(1, 2)).toBe(3); +}); + test('supports imports from "data:application/wasm" URI with base64 encoding', async () => { const importedWasmModule = await import( `data:application/wasm;base64,${wasmFileBuffer.toString('base64')}` From 397bcd113fe4d8334611b6d5a415f6be367bc7fc Mon Sep 17 00:00:00 2001 From: Alexander Kachkaev Date: Tue, 1 Nov 2022 19:34:21 +0000 Subject: [PATCH 19/23] Extract `isWasm` function --- packages/jest-runtime/src/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 0d9bf4a2ca62..bea182dd1a42 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -134,6 +134,8 @@ const getModuleNameMapper = (config: Config.ProjectConfig) => { return null; }; +const isWasm = (modulePath: string): boolean => modulePath.endsWith('.wasm'); + const unmockRegExpCache = new WeakMap(); const EVAL_RESULT_VARIABLE = 'Object.'; @@ -397,11 +399,11 @@ export default class Runtime { } // unstable as it should be replaced by https://github.com/nodejs/modules/issues/393, and we don't want people to use it - unstable_shouldLoadAsEsm(path: string): boolean { + unstable_shouldLoadAsEsm(modulePath: string): boolean { return ( - path.endsWith('.wasm') || + isWasm(modulePath) || Resolver.unstable_shouldLoadAsEsm( - path, + modulePath, this._config.extensionsToTreatAsEsm, ) ); @@ -444,7 +446,7 @@ export default class Runtime { 'Promise initialization should be sync - please report this bug to Jest!', ); - if (modulePath.endsWith('.wasm')) { + if (isWasm(modulePath)) { const wasm = this._importWasmModule( this.readFileBuffer(modulePath), modulePath, From 66a23125228767fb876a70347c31d7a63ac0b248 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 Nov 2022 20:26:56 +0100 Subject: [PATCH 20/23] Revert "Implement `readFileBuffer`" This reverts commit 0d4945e45bf3297ffd190bf89c9aa41083025067. --- docs/CodeTransformation.md | 2 +- packages/babel-jest/src/__tests__/index.ts | 8 +++---- packages/jest-repl/src/cli/repl.ts | 2 +- packages/jest-runtime/src/index.ts | 28 +++------------------- 4 files changed, 9 insertions(+), 31 deletions(-) diff --git a/docs/CodeTransformation.md b/docs/CodeTransformation.md index 3be3e0ac9a0e..2d16208eb10f 100644 --- a/docs/CodeTransformation.md +++ b/docs/CodeTransformation.md @@ -40,7 +40,7 @@ interface TransformOptions { supportsTopLevelAwait: boolean; instrument: boolean; /** Cached file system which is used by `jest-runtime` to improve performance. */ - cacheFS: Map; + cacheFS: Map; /** Jest configuration of currently running project. */ config: ProjectConfig; /** Stringified version of the `config` - useful in cache busting. */ diff --git a/packages/babel-jest/src/__tests__/index.ts b/packages/babel-jest/src/__tests__/index.ts index 4808bd0e591a..9903c2b773b0 100644 --- a/packages/babel-jest/src/__tests__/index.ts +++ b/packages/babel-jest/src/__tests__/index.ts @@ -52,7 +52,7 @@ test('Returns source string with inline maps when no transformOptions is passed' sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, @@ -76,7 +76,7 @@ test('Returns source string with inline maps when no transformOptions is passed sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, @@ -141,7 +141,7 @@ describe('caller option correctly merges from defaults and options', () => { ], ])('%j -> %j', (input, output) => { defaultBabelJestTransformer.process(sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, @@ -166,7 +166,7 @@ describe('caller option correctly merges from defaults and options', () => { test('can pass null to createTransformer', () => { const transformer = createTransformer(); transformer.process(sourceString, 'dummy_path.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: makeProjectConfig(), configString: JSON.stringify(makeProjectConfig()), instrument: false, diff --git a/packages/jest-repl/src/cli/repl.ts b/packages/jest-repl/src/cli/repl.ts index aceb9f1fb180..3a486c6bfbb7 100644 --- a/packages/jest-repl/src/cli/repl.ts +++ b/packages/jest-repl/src/cli/repl.ts @@ -33,7 +33,7 @@ const evalCommand: repl.REPLEval = ( cmd, jestGlobalConfig.replname ?? 'jest.js', { - cacheFS: new Map(), + cacheFS: new Map(), config: jestProjectConfig, configString: JSON.stringify(jestProjectConfig), instrument: false, diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index bea182dd1a42..95c49cc2a43a 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -155,7 +155,7 @@ const supportsNodeColonModulePrefixInRequire = (() => { })(); export default class Runtime { - private readonly _cacheFS: Map; + private readonly _cacheFS: Map; private readonly _config: Config.ProjectConfig; private readonly _globalConfig?: Config.GlobalConfig; private readonly _coverageOptions: ShouldInstrumentOptions; @@ -213,7 +213,7 @@ export default class Runtime { environment: JestEnvironment, resolver: Resolver, transformer: ScriptTransformer, - cacheFS: Map, + cacheFS: Map, coverageOptions: ShouldInstrumentOptions, testPath: string, // TODO: make mandatory in Jest 30 @@ -448,7 +448,7 @@ export default class Runtime { if (isWasm(modulePath)) { const wasm = this._importWasmModule( - this.readFileBuffer(modulePath), + fs.readFileSync(modulePath), modulePath, context, ); @@ -2378,23 +2378,6 @@ export default class Runtime { }; } - private readFileBuffer(filename: string): Buffer { - let source = this._cacheFS.get(filename); - - if (!source) { - source = fs.readFileSync(filename); - - this._cacheFS.set(filename, source); - } - - if (typeof source === 'string') { - // This may occur if readFileBuffer and readFile are called interchangeably for one file - throw new Error("Expected source to be a 'Buffer', but got 'string'"); - } - - return source; - } - private readFile(filename: string): string { let source = this._cacheFS.get(filename); @@ -2404,11 +2387,6 @@ export default class Runtime { this._cacheFS.set(filename, source); } - if (typeof source !== 'string') { - // This may occur if readFileBuffer and readFile are called interchangeably for one file - throw new Error("Expected source to be a 'string', but got 'Buffer'"); - } - return source; } From 7188086eebaec5861109a147df4d5f71a15ba7c2 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 Nov 2022 20:29:44 +0100 Subject: [PATCH 21/23] separate buffer cache --- packages/jest-runtime/src/index.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 95c49cc2a43a..c23b2a0bc64e 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -156,6 +156,7 @@ const supportsNodeColonModulePrefixInRequire = (() => { export default class Runtime { private readonly _cacheFS: Map; + private readonly _cacheFSBuffer = new Map(); private readonly _config: Config.ProjectConfig; private readonly _globalConfig?: Config.GlobalConfig; private readonly _coverageOptions: ShouldInstrumentOptions; @@ -448,7 +449,7 @@ export default class Runtime { if (isWasm(modulePath)) { const wasm = this._importWasmModule( - fs.readFileSync(modulePath), + this.readFileBuffer(modulePath), modulePath, context, ); @@ -2378,11 +2379,24 @@ export default class Runtime { }; } + private readFileBuffer(filename: string): Buffer { + let source = this._cacheFSBuffer.get(filename); + + if (!source) { + source = fs.readFileSync(filename); + + this._cacheFSBuffer.set(filename, source); + } + + return source; + } + private readFile(filename: string): string { let source = this._cacheFS.get(filename); if (!source) { - source = fs.readFileSync(filename, 'utf8'); + const buffer = this.readFileBuffer(filename); + source = buffer.toString('utf8'); this._cacheFS.set(filename, source); } From 26e7022706d928d24faf84e38270b7a3d66e9399 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 Nov 2022 20:33:19 +0100 Subject: [PATCH 22/23] tweak --- e2e/native-esm/__tests__/native-esm-wasm.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/e2e/native-esm/__tests__/native-esm-wasm.test.js b/e2e/native-esm/__tests__/native-esm-wasm.test.js index 853122085b4c..a48d6628eeff 100644 --- a/e2e/native-esm/__tests__/native-esm-wasm.test.js +++ b/e2e/native-esm/__tests__/native-esm-wasm.test.js @@ -5,9 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -// the point here is that it's the node core module -// eslint-disable-next-line no-restricted-imports -import {readFileSync} from 'fs'; +import {readFileSync} from 'node:fs'; // 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'; From 607fc0f53bb48d104fecc77e380a2bf33f89f235 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Sun, 6 Nov 2022 20:37:24 +0100 Subject: [PATCH 23/23] clear new cache as well --- packages/jest-runtime/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index c23b2a0bc64e..5dcf6d7ce6c8 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -1147,6 +1147,7 @@ export default class Runtime { this._cjsNamedExports.clear(); this._moduleMockRegistry.clear(); this._cacheFS.clear(); + this._cacheFSBuffer.clear(); if ( this._coverageOptions.collectCoverage &&