Skip to content

Commit

Permalink
feat: ESM data uri import and mock (#12392)
Browse files Browse the repository at this point in the history
  • Loading branch information
tbossi committed Feb 16, 2022
1 parent 4d573e4 commit adf8526
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@
- `[jest-environment-node]` [**BREAKING**] Add default `node` and `node-addon` conditions to `exportConditions` for `node` environment ([#11924](https://github.com/facebook/jest/pull/11924))
- `[@jest/expect-utils]` New module exporting utils for `expect` ([#12323](https://github.com/facebook/jest/pull/12323))
- `[jest-resolver]` [**BREAKING**] Add support for `package.json` `exports` ([11961](https://github.com/facebook/jest/pull/11961))
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
- `[@jes/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384))
- `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343))

Expand Down
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
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
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: 32 passed, 32 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites matching /native-esm.test.js/i."
Expand Down
95 changes: 95 additions & 0 deletions e2e/native-esm/__tests__/native-esm.test.js
Expand Up @@ -195,3 +195,98 @@ test('can mock module', async () => {
test('supports imports using "node:" prefix', () => {
expect(dns).toBe(prefixDns);
});

test('supports imports from "data:text/javascript" URI with charset=utf-8 encoding', 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" URI with base64 encoding', async () => {
const code = 'export const something = "some value"';
const importedBase64 = await import(
`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`
);
expect(importedBase64.something).toBe('some value');
});

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

test('imports from "data:text/javascript" URI with invalid encoding fail', async () => {
const code = 'export const something = "some value"';
await expect(
async () =>
await import(
`data:text/javascript;charset=badEncoding,${encodeURIComponent(code)}`
),
).rejects.toThrow('Invalid data URI');
});

test('imports from "data:" URI with invalid mime type fail', async () => {
const code = 'export const something = "some value"';
await expect(
async () => await import(`data:something/else,${encodeURIComponent(code)}`),
).rejects.toThrow('Invalid data URI');
});

test('imports from "data:text/javascript" URI with invalid data fail', async () => {
await expect(
async () =>
await import('data:text/javascript;charset=utf-8,so(me)+.-gibberish'),
).rejects.toThrow("Unexpected token '.'");
});

test('imports from "data:application/wasm" URI not supported', async () => {
await expect(
async () => await import('data:application/wasm,96cafe00babe'),
).rejects.toThrow('WASM 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'});
});

test('supports static "data:" URI import', async () => {
const module = await import('../staticDataImport.js');
expect(module.value()).toEqual({bar: {obj: 456}, foo: '123'});
});

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,${Buffer.from(code).toString('base64')}`
);
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,${Buffer.from(code).toString(
'base64',
)}`;
jestObject.unstable_mockModule(dataModule, () => ({foo: 'bar'}), {
virtual: true,
});
const mocked = await import(dataModule);
expect(mocked.foo).toBe('bar');
});
13 changes: 13 additions & 0 deletions e2e/native-esm/staticDataImport.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 bar from 'data:application/json,{"obj": 456}';
import {foo} from 'data:text/javascript,export const foo = "123"';

export function value() {
return {bar, foo};
}
3 changes: 3 additions & 0 deletions packages/jest-resolve/src/resolver.ts
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
83 changes: 83 additions & 0 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -63,6 +63,9 @@ export type {Context} from './types';

const esmIsAvailable = typeof SourceTextModule === 'function';

const dataURIregex =
/^data:(?<mime>text\/javascript|application\/json|application\/wasm)(?:;(?<encoding>charset=utf-8|base64))?,(?<code>.*)$/;

interface JestGlobals extends Global.TestFrameworkGlobals {
expect: typeof JestGlobals.expect;
}
Expand Down Expand Up @@ -553,6 +556,86 @@ 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(dataURIregex);

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

const mime = match.groups.mime;
if (mime === 'application/wasm') {
throw new Error('WASM 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},
);
} 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) {
meta.url = specifier;
},
});
}

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

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

0 comments on commit adf8526

Please sign in to comment.