diff --git a/CHANGELOG.md b/CHANGELOG.md index f85d3f887a47..497e1145a06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,13 @@ ### Fixes -- `[jest-environment-node]` fix non-configurable globals ([#13687](https://github.com/facebook/jest/pull/13687)) +- `[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-mock]` fix mockReset and resetAllMocks undefined return ([#13692](https://github.com/facebook/jest/pull/13692)) +- `[jest-mock]` Fix `mockReset` and `resetAllMocks` `undefined` return value([#13692](https://github.com/facebook/jest/pull/13692)) - `[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-runtime]` Using the scriptTransformer cache in jest-runner ([#13735](https://github.com/facebook/jest/pull/13735)) +- `[jest-runtime]` Support WASM files that import JS resources ([#13608](https://github.com/facebook/jest/pull/13608)) +- `[jest-runtime]` Use the `scriptTransformer` cache in `jest-runner` ([#13735](https://github.com/facebook/jest/pull/13735)) +- `[jest-runtime]` Enforce import assertions when importing JSON in ESM ([#12755](https://github.com/facebook/jest/pull/12755)) - `[jest-snapshot]` Make sure to import `babel` outside of the sandbox ([#13694](https://github.com/facebook/jest/pull/13694)) - `[jest-transform]` Ensure the correct configuration is passed to preprocessors specified multiple times in the `transform` option ([#13770](https://github.com/facebook/jest/pull/13770)) diff --git a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap index e711c1a45c02..e98ffe6d8f5b 100644 --- a/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap +++ b/e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap @@ -1,5 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`on node <16.12.0 does not enforce import assertions 1`] = ` +"Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /native-esm-missing-import-assertions.test/i." +`; + exports[`on node >=16.9.0 support re-exports from CJS of dual packages 1`] = ` "Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total @@ -8,6 +16,14 @@ Time: <> Ran all test suites matching /native-esm-deep-cjs-reexport.test.js/i." `; +exports[`on node >=16.12.0 supports import assertions 1`] = ` +"Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /native-esm-import-assertions.test/i." +`; + exports[`runs WebAssembly (Wasm) test with native ESM 1`] = ` "Test Suites: 1 passed, 1 total Tests: 6 passed, 6 total diff --git a/e2e/__tests__/nativeEsm.test.ts b/e2e/__tests__/nativeEsm.test.ts index a25d2dbd11c7..47ee045d0a3e 100644 --- a/e2e/__tests__/nativeEsm.test.ts +++ b/e2e/__tests__/nativeEsm.test.ts @@ -79,3 +79,66 @@ test('runs WebAssembly (Wasm) test with native ESM', () => { expect(stdout).toBe(''); expect(exitCode).toBe(0); }); + +// version where `vm` API gets `import assertions` +onNodeVersions('>=16.12.0', () => { + test('enforces import assertions', () => { + const {exitCode, stderr, stdout} = runJest( + DIR, + ['native-esm-missing-import-assertions.test'], + {nodeOptions: '--experimental-vm-modules --no-warnings'}, + ); + + const {rest} = extractSummary(stderr); + + expect(rest).toContain( + 'package.json" needs an import assertion of type "json"', + ); + expect(stdout).toBe(''); + expect(exitCode).toBe(1); + }); + + test('supports import assertions', () => { + const {exitCode, stderr, stdout} = runJest( + DIR, + ['native-esm-import-assertions.test'], + {nodeOptions: '--experimental-vm-modules --no-warnings'}, + ); + + const {summary} = extractSummary(stderr); + + expect(summary).toMatchSnapshot(); + expect(stdout).toBe(''); + expect(exitCode).toBe(0); + }); +}); + +onNodeVersions('<16.12.0', () => { + test('does not enforce import assertions', () => { + const {exitCode, stderr, stdout} = runJest( + DIR, + ['native-esm-missing-import-assertions.test'], + {nodeOptions: '--experimental-vm-modules --no-warnings'}, + ); + + const {summary} = extractSummary(stderr); + + expect(summary).toMatchSnapshot(); + expect(stdout).toBe(''); + expect(exitCode).toBe(0); + }); + + test('syntax error for import assertions', () => { + const {exitCode, stderr, stdout} = runJest( + DIR, + ['native-esm-import-assertions.test'], + {nodeOptions: '--experimental-vm-modules --no-warnings'}, + ); + + const {rest} = extractSummary(stderr); + + expect(rest).toContain('SyntaxError: Unexpected identifier'); + expect(stdout).toBe(''); + expect(exitCode).toBe(1); + }); +}); diff --git a/e2e/native-esm/__tests__/native-esm-import-assertions.test.js b/e2e/native-esm/__tests__/native-esm-import-assertions.test.js new file mode 100644 index 000000000000..2f390c49a079 --- /dev/null +++ b/e2e/native-esm/__tests__/native-esm-import-assertions.test.js @@ -0,0 +1,19 @@ +/** + * 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 json from '../package.json' assert {type: 'json'}; + +test('supports static import', () => { + expect(json).toHaveProperty('jest.testEnvironment', 'node'); +}); + +test('supports dynamic import', async () => { + const {default: json} = await import('../package.json', { + assert: {type: 'json'}, + }); + expect(json).toHaveProperty('jest.testEnvironment', 'node'); +}); diff --git a/e2e/native-esm/__tests__/native-esm-missing-import-assertions.test.js b/e2e/native-esm/__tests__/native-esm-missing-import-assertions.test.js new file mode 100644 index 000000000000..fabcb4624fbf --- /dev/null +++ b/e2e/native-esm/__tests__/native-esm-missing-import-assertions.test.js @@ -0,0 +1,17 @@ +/** + * 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 json from '../package.json'; + +test('supports static import', () => { + expect(json).toHaveProperty('jest.testEnvironment', 'node'); +}); + +test('supports dynamic import', async () => { + const {default: json} = await import('../package.json'); + expect(json).toHaveProperty('jest.testEnvironment', 'node'); +}); diff --git a/packages/jest-runtime/package.json b/packages/jest-runtime/package.json index 779e7b0b47d4..00e4db393165 100644 --- a/packages/jest-runtime/package.json +++ b/packages/jest-runtime/package.json @@ -37,6 +37,7 @@ "jest-resolve": "workspace:^", "jest-snapshot": "workspace:^", "jest-util": "workspace:^", + "semver": "^7.3.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -44,6 +45,7 @@ "@jest/test-utils": "workspace:^", "@types/glob": "^7.1.1", "@types/graceful-fs": "^4.1.3", + "@types/semver": "^7.1.0", "jest-environment-node": "workspace:^" }, "engines": { diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 3f3649f27dd6..237040006c05 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -21,6 +21,7 @@ import { import {parse as parseCjs} from 'cjs-module-lexer'; import {CoverageInstrumenter, V8Coverage} from 'collect-v8-coverage'; import * as fs from 'graceful-fs'; +import {satisfies as semverSatisfies} from 'semver'; import slash = require('slash'); import stripBOM = require('strip-bom'); import type { @@ -62,6 +63,11 @@ import { const esmIsAvailable = typeof SourceTextModule === 'function'; +const runtimeSupportsImportAssertions = semverSatisfies( + process.versions.node, + '^16.12.0 || >=17.0.0', +); + const dataURIRegex = /^data:(?text\/javascript|application\/json|application\/wasm)(?:;(?charset=utf-8|base64))?,(?.*)$/; @@ -409,11 +415,26 @@ export default class Runtime { ); } - // not async _now_, but transform will be private async loadEsmModule( modulePath: string, query = '', + importAssertions: ImportAssertions = {}, ): Promise { + if ( + runtimeSupportsImportAssertions && + modulePath.endsWith('.json') && + importAssertions.type !== 'json' + ) { + const error: NodeJS.ErrnoException = new Error( + `Module "${ + modulePath + (query ? `?${query}` : '') + }" needs an import assertion of type "json"`, + ); + error.code = 'ERR_IMPORT_ASSERTION_TYPE_MISSING'; + + throw error; + } + const cacheKey = modulePath + query; if (this._fileTransformsMutex.has(cacheKey)) { @@ -451,6 +472,7 @@ export default class Runtime { this.readFileBuffer(modulePath), modulePath, context, + importAssertions, ); this._esmoduleRegistry.set(cacheKey, wasm); @@ -477,39 +499,54 @@ export default class Runtime { }); try { - const module = new SourceTextModule(transformedCode, { - context, - identifier: modulePath, - 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 module; + if (modulePath.endsWith('.json')) { + module = new SyntheticModule( + ['default'], + function () { + const obj = JSON.parse(transformedCode); + // @ts-expect-error: TS doesn't know what `this` is + this.setExport('default', obj); + }, + {context, identifier: modulePath}, + ); + } else { + module = new SourceTextModule(transformedCode, { + context, + identifier: modulePath, + importModuleDynamically: async ( + specifier: string, + referencingModule: VMModule, + importAssertions?: ImportAssertions, + ) => { + 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, + importAssertions, + ); - return this.linkAndEvaluateModule(module); - }, - initializeImportMeta: (meta: JestImportMeta) => { - meta.url = pathToFileURL(modulePath).href; + return this.linkAndEvaluateModule(module); + }, + initializeImportMeta: (meta: JestImportMeta) => { + meta.url = pathToFileURL(modulePath).href; - let jest = this.jestObjectCaches.get(modulePath); + let jest = this.jestObjectCaches.get(modulePath); - if (!jest) { - jest = this._createJestObjectFor(modulePath); + if (!jest) { + jest = this._createJestObjectFor(modulePath); - this.jestObjectCaches.set(modulePath, jest); - } + this.jestObjectCaches.set(modulePath, jest); + } - meta.jest = jest; - }, - }); + meta.jest = jest; + }, + }); + } invariant( !this._esmoduleRegistry.has(cacheKey), @@ -539,6 +576,7 @@ export default class Runtime { specifier: string, referencingIdentifier: string, context: VMContext, + importAssertions: ImportAssertions = {}, ): Promise { if (this.isTornDown) { this._logFormattedReferenceError( @@ -599,6 +637,7 @@ export default class Runtime { Buffer.from(match.groups.code, 'base64'), specifier, context, + importAssertions, ); } else { let code = match.groups.code; @@ -627,6 +666,7 @@ export default class Runtime { importModuleDynamically: async ( specifier: string, referencingModule: VMModule, + importAssertions?: ImportAssertions, ) => { invariant( runtimeSupportsVmModules, @@ -636,6 +676,7 @@ export default class Runtime { specifier, referencingModule.identifier, referencingModule.context, + importAssertions, ); return this.linkAndEvaluateModule(module); @@ -672,9 +713,11 @@ export default class Runtime { if ( this._resolver.isCoreModule(resolved) || - this.unstable_shouldLoadAsEsm(resolved) + this.unstable_shouldLoadAsEsm(resolved) || + // json files are modules when imported in modules + resolved.endsWith('.json') ) { - return this.loadEsmModule(resolved, query); + return this.loadEsmModule(resolved, query, importAssertions); } return this.loadCjsAsEsm(referencingIdentifier, resolved, context); @@ -696,12 +739,18 @@ export default class Runtime { // this method can await it this._esmModuleLinkingMap.set( module, - module.link((specifier: string, referencingModule: VMModule) => - this.resolveModule( - specifier, - referencingModule.identifier, - referencingModule.context, - ), + module.link( + ( + specifier: string, + referencingModule: VMModule, + importCallOptions?: ImportCallOptions, + ) => + this.resolveModule( + specifier, + referencingModule.identifier, + referencingModule.context, + importCallOptions?.assert, + ), ), ); } @@ -1628,7 +1677,11 @@ export default class Runtime { displayErrors: true, filename: scriptFilename, // @ts-expect-error: Experimental ESM API - importModuleDynamically: async (specifier: string) => { + importModuleDynamically: async ( + specifier: string, + _script: Script, + importAssertions?: ImportAssertions, + ) => { 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', @@ -1642,6 +1695,7 @@ export default class Runtime { specifier, scriptFilename, context, + importAssertions, ); return this.linkAndEvaluateModule(module); @@ -1693,6 +1747,7 @@ export default class Runtime { source: Buffer, identifier: string, context: VMContext, + importAssertions: ImportAssertions | undefined, ) { const wasmModule = await WebAssembly.compile(source); @@ -1706,6 +1761,7 @@ export default class Runtime { module, identifier, context, + importAssertions, ); moduleLookup[module] = await this.linkAndEvaluateModule(resolvedModule); diff --git a/yarn.lock b/yarn.lock index 0661dfdb5a64..6d9924191168 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13014,6 +13014,7 @@ __metadata: "@types/glob": ^7.1.1 "@types/graceful-fs": ^4.1.3 "@types/node": "*" + "@types/semver": ^7.1.0 chalk: ^4.0.0 cjs-module-lexer: ^1.0.0 collect-v8-coverage: ^1.0.0 @@ -13027,6 +13028,7 @@ __metadata: jest-resolve: "workspace:^" jest-snapshot: "workspace:^" jest-util: "workspace:^" + semver: ^7.3.5 slash: ^3.0.0 strip-bom: ^4.0.0 languageName: unknown