Skip to content

Commit b404296

Browse files
authored
chore: add validation of import assertions (#13805)
1 parent 6f8e918 commit b404296

File tree

3 files changed

+202
-15
lines changed

3 files changed

+202
-15
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
- `[jest-resolve]` Add global paths to `require.resolve.paths` ([#13633](https://github.com/facebook/jest/pull/13633))
2121
- `[jest-runtime]` Support WASM files that import JS resources ([#13608](https://github.com/facebook/jest/pull/13608))
2222
- `[jest-runtime]` Use the `scriptTransformer` cache in `jest-runner` ([#13735](https://github.com/facebook/jest/pull/13735))
23-
- `[jest-runtime]` Enforce import assertions when importing JSON in ESM ([#12755](https://github.com/facebook/jest/pull/12755))
23+
- `[jest-runtime]` Enforce import assertions when importing JSON in ESM ([#12755](https://github.com/facebook/jest/pull/12755) & [#13805](https://github.com/facebook/jest/pull/13805))
2424
- `[jest-snapshot]` Make sure to import `babel` outside of the sandbox ([#13694](https://github.com/facebook/jest/pull/13694))
2525
- `[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))
2626

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import {pathToFileURL} from 'url';
10+
import {onNodeVersions} from '@jest/test-utils';
11+
12+
let runtime;
13+
14+
// version where `vm` API gets `import assertions`
15+
onNodeVersions('>=16.12.0', () => {
16+
beforeAll(async () => {
17+
const createRuntime = require('createRuntime');
18+
19+
runtime = await createRuntime(__filename);
20+
});
21+
22+
describe('import assertions', () => {
23+
const fileUrl = pathToFileURL(__filename).href;
24+
const jsonFileName = `${__filename}on`;
25+
const jsonFileUrl = pathToFileURL(jsonFileName).href;
26+
27+
it('works if passed correct import assertion', () => {
28+
expect(() =>
29+
runtime.validateImportAssertions(jsonFileName, '', {type: 'json'}),
30+
).not.toThrow();
31+
});
32+
33+
it('does nothing if no assertions passed for js file', () => {
34+
expect(() =>
35+
runtime.validateImportAssertions(__filename, '', undefined),
36+
).not.toThrow();
37+
expect(() =>
38+
runtime.validateImportAssertions(__filename, '', {}),
39+
).not.toThrow();
40+
});
41+
42+
it('throws if invalid assertions are passed', () => {
43+
expect(() =>
44+
runtime.validateImportAssertions(jsonFileName, '', {type: null}),
45+
).toThrow('Import assertion value must be a string');
46+
expect(() =>
47+
runtime.validateImportAssertions(jsonFileName, '', {type: 42}),
48+
).toThrow('Import assertion value must be a string');
49+
expect(() =>
50+
runtime.validateImportAssertions(jsonFileName, '', {
51+
type: 'javascript',
52+
}),
53+
).toThrow('Import assertion type "javascript" is unsupported');
54+
});
55+
56+
it('throws if missing json assertions', () => {
57+
const errorMessage = `Module "${jsonFileUrl}" needs an import assertion of type "json"`;
58+
59+
expect(() =>
60+
runtime.validateImportAssertions(jsonFileName, '', {}),
61+
).toThrow(errorMessage);
62+
expect(() =>
63+
runtime.validateImportAssertions(jsonFileName, '', {
64+
somethingElse: 'json',
65+
}),
66+
).toThrow(errorMessage);
67+
expect(() => runtime.validateImportAssertions(jsonFileName, '')).toThrow(
68+
errorMessage,
69+
);
70+
});
71+
72+
it('throws if json assertion passed on wrong file', () => {
73+
expect(() =>
74+
runtime.validateImportAssertions(__filename, '', {type: 'json'}),
75+
).toThrow(`Module "${fileUrl}" is not of type "json"`);
76+
});
77+
});
78+
});

packages/jest-runtime/src/index.ts

+123-14
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,23 @@ const supportsNodeColonModulePrefixInRequire = (() => {
159159
}
160160
})();
161161

162+
const kImplicitAssertType = Symbol('kImplicitAssertType');
163+
164+
// copied from https://github.com/nodejs/node/blob/7dd458382580f68cf7d718d96c8f4d2d3fe8b9db/lib/internal/modules/esm/assert.js#L20-L32
165+
const formatTypeMap: {[type: string]: string | typeof kImplicitAssertType} = {
166+
// @ts-expect-error - copied
167+
__proto__: null,
168+
builtin: kImplicitAssertType,
169+
commonjs: kImplicitAssertType,
170+
json: 'json',
171+
module: kImplicitAssertType,
172+
wasm: kImplicitAssertType,
173+
};
174+
175+
const supportedAssertionTypes = new Set(
176+
Object.values(formatTypeMap).filter(type => type !== kImplicitAssertType),
177+
);
178+
162179
export default class Runtime {
163180
private readonly _cacheFS: Map<string, string>;
164181
private readonly _cacheFSBuffer = new Map<string, Buffer>();
@@ -418,21 +435,10 @@ export default class Runtime {
418435
private async loadEsmModule(
419436
modulePath: string,
420437
query = '',
421-
importAssertions: ImportAssertions = {},
438+
importAssertions?: ImportAssertions,
422439
): Promise<VMModule> {
423-
if (
424-
runtimeSupportsImportAssertions &&
425-
modulePath.endsWith('.json') &&
426-
importAssertions.type !== 'json'
427-
) {
428-
const error: NodeJS.ErrnoException = new Error(
429-
`Module "${
430-
modulePath + (query ? `?${query}` : '')
431-
}" needs an import assertion of type "json"`,
432-
);
433-
error.code = 'ERR_IMPORT_ASSERTION_TYPE_MISSING';
434-
435-
throw error;
440+
if (runtimeSupportsImportAssertions) {
441+
this.validateImportAssertions(modulePath, query, importAssertions);
436442
}
437443

438444
const cacheKey = modulePath + query;
@@ -572,6 +578,83 @@ export default class Runtime {
572578
return module;
573579
}
574580

581+
private validateImportAssertions(
582+
modulePath: string,
583+
query: string,
584+
importAssertions: ImportAssertions = {
585+
// @ts-expect-error - copy https://github.com/nodejs/node/blob/7dd458382580f68cf7d718d96c8f4d2d3fe8b9db/lib/internal/modules/esm/assert.js#LL55C50-L55C65
586+
__proto__: null,
587+
},
588+
) {
589+
const format = this.getModuleFormat(modulePath);
590+
const validType = formatTypeMap[format];
591+
const url = pathToFileURL(modulePath);
592+
593+
if (query) {
594+
url.search = query;
595+
}
596+
597+
const urlString = url.href;
598+
599+
const assertionType = importAssertions.type;
600+
601+
switch (validType) {
602+
case undefined:
603+
// Ignore assertions for module formats we don't recognize, to allow new
604+
// formats in the future.
605+
return;
606+
607+
case kImplicitAssertType:
608+
// This format doesn't allow an import assertion type, so the property
609+
// must not be set on the import assertions object.
610+
if (Object.prototype.hasOwnProperty.call(importAssertions, 'type')) {
611+
handleInvalidAssertionType(urlString, assertionType);
612+
}
613+
return;
614+
615+
case assertionType:
616+
// The asserted type is the valid type for this format.
617+
return;
618+
619+
default:
620+
// There is an expected type for this format, but the value of
621+
// `importAssertions.type` might not have been it.
622+
if (!Object.prototype.hasOwnProperty.call(importAssertions, 'type')) {
623+
// `type` wasn't specified at all.
624+
const error: NodeJS.ErrnoException = new Error(
625+
`Module "${urlString}" needs an import assertion of type "json"`,
626+
);
627+
error.code = 'ERR_IMPORT_ASSERTION_TYPE_MISSING';
628+
629+
throw error;
630+
}
631+
handleInvalidAssertionType(urlString, assertionType);
632+
}
633+
}
634+
635+
private getModuleFormat(modulePath: string) {
636+
if (this._resolver.isCoreModule(modulePath)) {
637+
return 'builtin';
638+
}
639+
640+
if (isWasm(modulePath)) {
641+
return 'wasm';
642+
}
643+
644+
const fileExtension = path.extname(modulePath);
645+
646+
if (fileExtension === '.json') {
647+
return 'json';
648+
}
649+
650+
if (this.unstable_shouldLoadAsEsm(modulePath)) {
651+
return 'module';
652+
}
653+
654+
// any unknown format should be treated as JS
655+
return 'commonjs';
656+
}
657+
575658
private async resolveModule<T = unknown>(
576659
specifier: string,
577660
referencingIdentifier: string,
@@ -2513,3 +2596,29 @@ async function evaluateSyntheticModule(module: SyntheticModule) {
25132596

25142597
return module;
25152598
}
2599+
2600+
function handleInvalidAssertionType(url: string, type: unknown) {
2601+
if (typeof type !== 'string') {
2602+
throw new TypeError('Import assertion value must be a string');
2603+
}
2604+
2605+
// `type` might not have been one of the types we understand.
2606+
if (!supportedAssertionTypes.has(type)) {
2607+
const error: NodeJS.ErrnoException = new Error(
2608+
`Import assertion type "${type}" is unsupported`,
2609+
);
2610+
2611+
error.code = 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED';
2612+
2613+
throw error;
2614+
}
2615+
2616+
// `type` was the wrong value for this format.
2617+
const error: NodeJS.ErrnoException = new Error(
2618+
`Module "${url}" is not of type "${type}"`,
2619+
);
2620+
2621+
error.code = 'ERR_IMPORT_ASSERTION_TYPE_FAILED';
2622+
2623+
throw error;
2624+
}

0 commit comments

Comments
 (0)