Skip to content

Commit

Permalink
chore: add validation of import assertions (#13805)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Jan 24, 2023
1 parent 6f8e918 commit b404296
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 15 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -20,7 +20,7 @@
- `[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]` 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-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))
- `[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
@@ -0,0 +1,78 @@
/**
* 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 {pathToFileURL} from 'url';
import {onNodeVersions} from '@jest/test-utils';

let runtime;

// version where `vm` API gets `import assertions`
onNodeVersions('>=16.12.0', () => {
beforeAll(async () => {
const createRuntime = require('createRuntime');

runtime = await createRuntime(__filename);
});

describe('import assertions', () => {
const fileUrl = pathToFileURL(__filename).href;
const jsonFileName = `${__filename}on`;
const jsonFileUrl = pathToFileURL(jsonFileName).href;

it('works if passed correct import assertion', () => {
expect(() =>
runtime.validateImportAssertions(jsonFileName, '', {type: 'json'}),
).not.toThrow();
});

it('does nothing if no assertions passed for js file', () => {
expect(() =>
runtime.validateImportAssertions(__filename, '', undefined),
).not.toThrow();
expect(() =>
runtime.validateImportAssertions(__filename, '', {}),
).not.toThrow();
});

it('throws if invalid assertions are passed', () => {
expect(() =>
runtime.validateImportAssertions(jsonFileName, '', {type: null}),
).toThrow('Import assertion value must be a string');
expect(() =>
runtime.validateImportAssertions(jsonFileName, '', {type: 42}),
).toThrow('Import assertion value must be a string');
expect(() =>
runtime.validateImportAssertions(jsonFileName, '', {
type: 'javascript',
}),
).toThrow('Import assertion type "javascript" is unsupported');
});

it('throws if missing json assertions', () => {
const errorMessage = `Module "${jsonFileUrl}" needs an import assertion of type "json"`;

expect(() =>
runtime.validateImportAssertions(jsonFileName, '', {}),
).toThrow(errorMessage);
expect(() =>
runtime.validateImportAssertions(jsonFileName, '', {
somethingElse: 'json',
}),
).toThrow(errorMessage);
expect(() => runtime.validateImportAssertions(jsonFileName, '')).toThrow(
errorMessage,
);
});

it('throws if json assertion passed on wrong file', () => {
expect(() =>
runtime.validateImportAssertions(__filename, '', {type: 'json'}),
).toThrow(`Module "${fileUrl}" is not of type "json"`);
});
});
});
137 changes: 123 additions & 14 deletions packages/jest-runtime/src/index.ts
Expand Up @@ -159,6 +159,23 @@ const supportsNodeColonModulePrefixInRequire = (() => {
}
})();

const kImplicitAssertType = Symbol('kImplicitAssertType');

// copied from https://github.com/nodejs/node/blob/7dd458382580f68cf7d718d96c8f4d2d3fe8b9db/lib/internal/modules/esm/assert.js#L20-L32
const formatTypeMap: {[type: string]: string | typeof kImplicitAssertType} = {
// @ts-expect-error - copied
__proto__: null,
builtin: kImplicitAssertType,
commonjs: kImplicitAssertType,
json: 'json',
module: kImplicitAssertType,
wasm: kImplicitAssertType,
};

const supportedAssertionTypes = new Set(
Object.values(formatTypeMap).filter(type => type !== kImplicitAssertType),
);

export default class Runtime {
private readonly _cacheFS: Map<string, string>;
private readonly _cacheFSBuffer = new Map<string, Buffer>();
Expand Down Expand Up @@ -418,21 +435,10 @@ export default class Runtime {
private async loadEsmModule(
modulePath: string,
query = '',
importAssertions: ImportAssertions = {},
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;
if (runtimeSupportsImportAssertions) {
this.validateImportAssertions(modulePath, query, importAssertions);
}

const cacheKey = modulePath + query;
Expand Down Expand Up @@ -572,6 +578,83 @@ export default class Runtime {
return module;
}

private validateImportAssertions(
modulePath: string,
query: string,
importAssertions: ImportAssertions = {
// @ts-expect-error - copy https://github.com/nodejs/node/blob/7dd458382580f68cf7d718d96c8f4d2d3fe8b9db/lib/internal/modules/esm/assert.js#LL55C50-L55C65
__proto__: null,
},
) {
const format = this.getModuleFormat(modulePath);
const validType = formatTypeMap[format];
const url = pathToFileURL(modulePath);

if (query) {
url.search = query;
}

const urlString = url.href;

const assertionType = importAssertions.type;

switch (validType) {
case undefined:
// Ignore assertions for module formats we don't recognize, to allow new
// formats in the future.
return;

case kImplicitAssertType:
// This format doesn't allow an import assertion type, so the property
// must not be set on the import assertions object.
if (Object.prototype.hasOwnProperty.call(importAssertions, 'type')) {
handleInvalidAssertionType(urlString, assertionType);
}
return;

case assertionType:
// The asserted type is the valid type for this format.
return;

default:
// There is an expected type for this format, but the value of
// `importAssertions.type` might not have been it.
if (!Object.prototype.hasOwnProperty.call(importAssertions, 'type')) {
// `type` wasn't specified at all.
const error: NodeJS.ErrnoException = new Error(
`Module "${urlString}" needs an import assertion of type "json"`,
);
error.code = 'ERR_IMPORT_ASSERTION_TYPE_MISSING';

throw error;
}
handleInvalidAssertionType(urlString, assertionType);
}
}

private getModuleFormat(modulePath: string) {
if (this._resolver.isCoreModule(modulePath)) {
return 'builtin';
}

if (isWasm(modulePath)) {
return 'wasm';
}

const fileExtension = path.extname(modulePath);

if (fileExtension === '.json') {
return 'json';
}

if (this.unstable_shouldLoadAsEsm(modulePath)) {
return 'module';
}

// any unknown format should be treated as JS
return 'commonjs';
}

private async resolveModule<T = unknown>(
specifier: string,
referencingIdentifier: string,
Expand Down Expand Up @@ -2513,3 +2596,29 @@ async function evaluateSyntheticModule(module: SyntheticModule) {

return module;
}

function handleInvalidAssertionType(url: string, type: unknown) {
if (typeof type !== 'string') {
throw new TypeError('Import assertion value must be a string');
}

// `type` might not have been one of the types we understand.
if (!supportedAssertionTypes.has(type)) {
const error: NodeJS.ErrnoException = new Error(
`Import assertion type "${type}" is unsupported`,
);

error.code = 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED';

throw error;
}

// `type` was the wrong value for this format.
const error: NodeJS.ErrnoException = new Error(
`Module "${url}" is not of type "${type}"`,
);

error.code = 'ERR_IMPORT_ASSERTION_TYPE_FAILED';

throw error;
}

0 comments on commit b404296

Please sign in to comment.