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(jest-util): add requireOrImportModule util for importing CJS or ESM #11199

Merged
merged 8 commits into from Mar 16, 2021
30 changes: 3 additions & 27 deletions packages/jest-config/src/readConfigFileAndSetRootDir.ts
Expand Up @@ -6,11 +6,10 @@
*/

import * as path from 'path';
import {pathToFileURL} from 'url';
import * as fs from 'graceful-fs';
import type {Register} from 'ts-node';
import type {Config} from '@jest/types';
import {interopRequireDefault} from 'jest-util';
import {interopRequireDefault, requireOrImportModule} from 'jest-util';
import {
JEST_CONFIG_EXT_JSON,
JEST_CONFIG_EXT_TS,
Expand All @@ -35,33 +34,10 @@ export default async function readConfigFileAndSetRootDir(
if (isTS) {
configObject = await loadTSConfigFile(configPath);
} else {
configObject = require(configPath);
configObject = await requireOrImportModule<any>(configPath);
}
} catch (error) {
if (error.code === 'ERR_REQUIRE_ESM') {
try {
const configUrl = pathToFileURL(configPath);

// node `import()` supports URL, but TypeScript doesn't know that
const importedConfig = await import(configUrl.href);

if (!importedConfig.default) {
throw new Error(
`Jest: Failed to load mjs config file ${configPath} - did you use a default export?`,
);
}

configObject = importedConfig.default;
} catch (innerError) {
if (innerError.message === 'Not supported') {
throw new Error(
`Jest: Your version of Node does not support dynamic import - please enable it or use a .cjs file extension for file ${configPath}`,
);
}
Comment on lines -56 to -60
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All LTS Node should support dynamic import after Node 10 EOL.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. Jest 27 will support Node 10, but 28 will drop it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we should keep the warning for now


throw innerError;
}
} else if (isJSON) {
if (isJSON) {
throw new Error(
`Jest: Failed to parse config file ${configPath}\n` +
` ${jsonlint.errors(fs.readFileSync(configPath, 'utf8'))}`,
Expand Down
27 changes: 4 additions & 23 deletions packages/jest-transform/src/ScriptTransformer.ts
Expand Up @@ -7,7 +7,6 @@

import {createHash} from 'crypto';
import * as path from 'path';
import {pathToFileURL} from 'url';
import {transformSync as babelTransform} from '@babel/core';
// @ts-expect-error: should just be `require.resolve`, but the tests mess that up
import babelPluginIstanbul from 'babel-plugin-istanbul';
Expand All @@ -23,6 +22,7 @@ import {
createDirectory,
interopRequireDefault,
isPromise,
requireOrImportModule,
tryRealpath,
} from 'jest-util';
import handlePotentialSyntaxError from './enhanceUnexpectedTokenMessage';
Expand Down Expand Up @@ -252,28 +252,9 @@ class ScriptTransformer {
await Promise.all(
this._config.transform.map(
async ([, transformPath, transformerConfig]) => {
let transformer: Transformer;

try {
transformer = interopRequireDefault(require(transformPath)).default;
} catch (error) {
if (error.code === 'ERR_REQUIRE_ESM') {
const configUrl = pathToFileURL(transformPath);

// node `import()` supports URL, but TypeScript doesn't know that
const importedConfig = await import(configUrl.href);

if (!importedConfig.default) {
throw new Error(
`Jest: Failed to load ESM transformer at ${transformPath} - did you use a default export?`,
);
}

transformer = importedConfig.default;
} else {
throw error;
}
}
let transformer: Transformer = await requireOrImportModule(
transformPath,
);

if (!transformer) {
throw new TypeError('Jest: a transform must export something.');
Expand Down
1 change: 1 addition & 0 deletions packages/jest-util/src/index.ts
Expand Up @@ -23,3 +23,4 @@ export * as preRunMessage from './preRunMessage';
export {default as pluralize} from './pluralize';
export {default as formatTime} from './formatTime';
export {default as tryRealpath} from './tryRealpath';
export {default as requireOrImportModule} from './requireOrImportModule';
50 changes: 50 additions & 0 deletions packages/jest-util/src/requireOrImportModule.ts
@@ -0,0 +1,50 @@
/**
* 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 {isAbsolute} from 'path';
import {pathToFileURL} from 'url';
import type {Config} from '@jest/types';
import interopRequireDefault from './interopRequireDefault';

export default async function requireOrImportModule<T>(
filePath: Config.Path,
): Promise<T> {
let module: T;
if (!isAbsolute(filePath) && filePath[0] === '.') {
throw new Error(`Jest: requireOrImportModule path must be absolute`);
}
try {
module = interopRequireDefault(require(filePath)).default;
} catch (error) {
if (error.code === 'ERR_REQUIRE_ESM') {
try {
const configUrl = pathToFileURL(filePath);

// node `import()` supports URL, but TypeScript doesn't know that
const importedConfig = await import(configUrl.href);

if (!importedConfig.default) {
throw new Error(
`Jest: Failed to load ESM at ${filePath} - did you use a default export?`,
);
}

module = importedConfig.default;
} catch (innerError) {
if (innerError.message === 'Not supported') {
throw new Error(
`Jest: Your version of Node does not support dynamic import - please enable it or use a .cjs file extension for file ${filePath}`,
);
}
throw innerError;
}
} else {
throw error;
}
}
return module;
}