Skip to content

Commit

Permalink
feat: add support for import assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Jan 24, 2023
1 parent e0b1249 commit ee3be53
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 39 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Expand Up @@ -19,7 +19,8 @@
- `[jest-mock]` fix mockReset and resetAllMocks undefined return ([#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]` Using 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))

Expand Down
8 changes: 8 additions & 0 deletions 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: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites matching /native-esm-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
Expand Down
34 changes: 34 additions & 0 deletions e2e/__tests__/nativeEsm.test.ts
Expand Up @@ -79,3 +79,37 @@ 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-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);
});
});
onNodeVersions('<16.12.0', () => {
test('does not enforce 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);
});
});
12 changes: 12 additions & 0 deletions e2e/native-esm/__tests__/native-esm-import-assertions.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 json from '../package.json';

test('supports top level await', () => {
expect(json).toHaveProperty('jest.testEnvironment', 'node');
});
2 changes: 2 additions & 0 deletions packages/jest-runtime/package.json
Expand Up @@ -37,13 +37,15 @@
"jest-resolve": "workspace:^",
"jest-snapshot": "workspace:^",
"jest-util": "workspace:^",
"semver": "^7.3.5",
"slash": "^3.0.0",
"strip-bom": "^4.0.0"
},
"devDependencies": {
"@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": {
Expand Down
132 changes: 94 additions & 38 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:(?<mime>text\/javascript|application\/json|application\/wasm)(?:;(?<encoding>charset=utf-8|base64))?,(?<code>.*)$/;

Expand Down Expand Up @@ -409,11 +415,26 @@ export default class Runtime {
);
}

// not async _now_, but transform will be
private async loadEsmModule(
modulePath: string,
query = '',
importAssertions: ImportAssertions = {},
): Promise<VMModule> {
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)) {
Expand Down Expand Up @@ -451,6 +472,7 @@ export default class Runtime {
this.readFileBuffer(modulePath),
modulePath,
context,
importAssertions,
);

this._esmoduleRegistry.set(cacheKey, wasm);
Expand All @@ -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),
Expand Down Expand Up @@ -539,6 +576,7 @@ export default class Runtime {
specifier: string,
referencingIdentifier: string,
context: VMContext,
importAssertions: ImportAssertions = {},
): Promise<T> {
if (this.isTornDown) {
this._logFormattedReferenceError(
Expand Down Expand Up @@ -599,6 +637,7 @@ export default class Runtime {
Buffer.from(match.groups.code, 'base64'),
specifier,
context,
importAssertions,
);
} else {
let code = match.groups.code;
Expand Down Expand Up @@ -627,6 +666,7 @@ export default class Runtime {
importModuleDynamically: async (
specifier: string,
referencingModule: VMModule,
importAssertions?: ImportAssertions,
) => {
invariant(
runtimeSupportsVmModules,
Expand All @@ -636,6 +676,7 @@ export default class Runtime {
specifier,
referencingModule.identifier,
referencingModule.context,
importAssertions,
);

return this.linkAndEvaluateModule(module);
Expand Down Expand Up @@ -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);
Expand All @@ -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,
),
),
);
}
Expand Down Expand Up @@ -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',
Expand All @@ -1642,6 +1695,7 @@ export default class Runtime {
specifier,
scriptFilename,
context,
importAssertions,
);

return this.linkAndEvaluateModule(module);
Expand Down Expand Up @@ -1693,6 +1747,7 @@ export default class Runtime {
source: Buffer,
identifier: string,
context: VMContext,
importAssertions: ImportAssertions | undefined,
) {
const wasmModule = await WebAssembly.compile(source);

Expand All @@ -1706,6 +1761,7 @@ export default class Runtime {
module,
identifier,
context,
importAssertions,
);

moduleLookup[module] = await this.linkAndEvaluateModule(resolvedModule);
Expand Down
2 changes: 2 additions & 0 deletions yarn.lock
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit ee3be53

Please sign in to comment.