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

feat: ESM data uri import and mock #12392

Merged
merged 12 commits into from
Feb 16, 2022
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ exports[`moduleNameMapper wrong array configuration 1`] = `
12 | module.exports = () => 'test';
13 |

at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:568:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:572:17)
at Object.require (index.js:10:1)"
`;

Expand Down Expand Up @@ -70,6 +70,6 @@ exports[`moduleNameMapper wrong configuration 1`] = `
12 | module.exports = () => 'test';
13 |

at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:568:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/resolver.js:572:17)
at Object.require (index.js:10:1)"
`;
2 changes: 1 addition & 1 deletion e2e/__tests__/__snapshots__/nativeEsm.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`on node >=12.16.0 runs test with native ESM 1`] = `
"Test Suites: 1 passed, 1 total
Tests: 21 passed, 21 total
Tests: 25 passed, 25 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites matching /native-esm.test.js/i."
Expand Down
71 changes: 57 additions & 14 deletions e2e/native-esm/__tests__/native-esm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@
* LICENSE file in the root directory of this source tree.
*/

import dns from 'dns';
import {jest as jestObject} from '@jest/globals'
import dns from 'dns'
// the point here is that it's the node core module
// eslint-disable-next-line no-restricted-imports
import {readFileSync} from 'fs';
import {createRequire} from 'module';
import prefixDns from 'node:dns';
import {dirname, resolve} from 'path';
import {fileURLToPath} from 'url';
import {jest as jestObject} from '@jest/globals';
import staticImportedStatefulFromCjs from '../fromCjs.mjs';
import {double} from '../index';
import defaultFromCjs, {half, namedFunction} from '../namedExport.cjs';
import {bag} from '../namespaceExport.js';
import {readFileSync} from 'fs'
SimenB marked this conversation as resolved.
Show resolved Hide resolved
import {createRequire} from 'module'
import prefixDns from 'node:dns'
import {dirname, resolve} from 'path'
import {fileURLToPath} from 'url'
import staticImportedStatefulFromCjs from '../fromCjs.mjs'
import {double} from '../index'
import defaultFromCjs, {half, namedFunction} from '../namedExport.cjs'
import {bag} from '../namespaceExport.js'
/* eslint-disable import/no-duplicates */
import staticImportedStateful from '../stateful.mjs';
import staticImportedStatefulWithQuery from '../stateful.mjs?query=1';
import staticImportedStatefulWithAnotherQuery from '../stateful.mjs?query=2';
import staticImportedStateful from '../stateful.mjs'
import staticImportedStatefulWithQuery from '../stateful.mjs?query=1'
import staticImportedStatefulWithAnotherQuery from '../stateful.mjs?query=2'
/* eslint-enable import/no-duplicates */

test('should have correct import.meta', () => {
Expand Down Expand Up @@ -195,3 +195,46 @@ test('can mock module', async () => {
test('supports imports using "node:" prefix', () => {
expect(dns).toBe(prefixDns);
});

test('supports imports from "data:text/javascript;charset=utf-8" URI', async () => {
const code = 'export const something = "some value"';
const importedEncoded = await import(
`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`
);
expect(importedEncoded.something).toBe('some value');
});

test('supports imports from "data:text/javascript;base64" URI', async () => {
const code = 'export const something = "some value"';
const importedBase64 = await import(
`data:text/javascript;base64,${btoa(code)}`
);
expect(importedBase64.something).toBe('some value');
});

test('imports from "data:" URI is properly cached', async () => {
const code =
'export const wrapper = {value: 123}\nexport const set = (value) => wrapper.value = value';
const data1 = await import(
`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`
);
expect(data1.wrapper.value).toBe(123);
data1.set(234);
expect(data1.wrapper.value).toBe(234);
const data2 = await import(`data:text/javascript;base64,${btoa(code)}`);
expect(data2.wrapper.value).toBe(123);
const data3 = await import(
`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`
);
expect(data3.wrapper.value).toBe(234);
});

test('can mock "data:" URI module', async () => {
const code = 'export const something = "some value"';
const dataModule = `data:text/javascript;base64,${btoa(code)}`;
jestObject.unstable_mockModule(dataModule, () => ({foo: 'bar'}), {
virtual: true,
});
const mocked = await import(dataModule);
expect(mocked.foo).toBe('bar');
});
3 changes: 3 additions & 0 deletions packages/jest-resolve/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,9 @@ export default class Resolver {
if (this.isCoreModule(moduleName)) {
return moduleName;
}
if (moduleName.startsWith('data:')) {
return moduleName;
}
return this._isModuleResolved(from, moduleName)
? this.getModule(moduleName)
: this._getVirtualMockPath(virtualMocks, from, moduleName, options);
Expand Down
63 changes: 63 additions & 0 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,69 @@ export default class Runtime {
return globals;
}

if (specifier.startsWith('data:')) {
if (
this._shouldMock(
referencingIdentifier,
specifier,
this._explicitShouldMockModule,
{conditions: this.esmConditions},
)
) {
return this.importMock(referencingIdentifier, specifier, context);
}

const fromCache = this._esmoduleRegistry.get(specifier);

if (fromCache) {
return fromCache;
}

const match = specifier.match(
/^data:text\/javascript;(charset=utf-8|base64),(.*)$/,
SimenB marked this conversation as resolved.
Show resolved Hide resolved
SimenB marked this conversation as resolved.
Show resolved Hide resolved
);

if (!match) {
throw new Error('Invalid data URI');
}

let code = match[2];
if (match[1] === 'base64') {
code = atob(code);
SimenB marked this conversation as resolved.
Show resolved Hide resolved
} else if (match[1] === 'charset=utf-8') {
code = decodeURIComponent(code);
} else {
throw new Error('Invalid data URI encoding');
tbossi marked this conversation as resolved.
Show resolved Hide resolved
}

const 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) {
meta.url = specifier;
},
});

this._esmoduleRegistry.set(specifier, module);
return module;
}

if (specifier.startsWith('file://')) {
specifier = fileURLToPath(specifier);
}
Expand Down