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

chore: add validation of import assertions #13805

Merged
merged 4 commits into from Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}