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

Add support for the "module" package.json field #5485

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,13 @@ Default: `undefined`
This option allows the use of a custom global teardown module which exports an
async function that is triggered once after all test suites.

### `module` [boolean]
Default: `false`

Respect the [`"module"` field](https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md) in `package.json`. Many libraries that contain ES modules ([import/export syntax](http://www.2ality.com/2014/09/es6-modules-final.html)) use this field as a pointer to their untranspiled (ES6+) source, in addition to the ubiquitous `"main"` field that points to precompiled ES5 javascript.

_TL;DR: enable this if your project uses import/export syntax._

### `moduleFileExtensions` [array<string>]

Default: `["js", "json", "jsx", "node"]`
Expand Down
3 changes: 3 additions & 0 deletions examples/module-field/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["env"]
}
16 changes: 16 additions & 0 deletions examples/module-field/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

import { connect } from 'react-redux';
import Contents from '../sample';

describe('module field resolution', () => {
it('should return the correct file exports', () => {
expect(typeof connect).toBe('function');
expect(Contents).toBe('👏');
});
});
16 changes: 16 additions & 0 deletions examples/module-field/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"devDependencies": {
"babel-preset-env": "*",
"jest": "*"
},
"jest": {
"module": true
},
"scripts": {
"test": "jest"
},
"dependencies": {
"react-redux": "^5.0.6",
"redux": "^3.7.2"
}
}
7 changes: 7 additions & 0 deletions examples/module-field/sample/correct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
export default '👏';
4 changes: 4 additions & 0 deletions examples/module-field/sample/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "wrong.js",
"module": "correct.js"
}
9 changes: 9 additions & 0 deletions examples/module-field/sample/wrong.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Copyright (c) 2017-present, Facebook, Inc. All rights reserved.
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';

module.exports = '😒';
1 change: 1 addition & 0 deletions packages/jest-config/src/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default ({
haste: {
providesModuleNodeModules: [],
},
module: false,
moduleDirectories: ['node_modules'],
moduleFileExtensions: ['js', 'json', 'jsx', 'node'],
moduleNameMapper: {},
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const getConfigs = (
forceCoverageMatch: options.forceCoverageMatch,
globals: options.globals,
haste: options.haste,
module: options.module,
moduleDirectories: options.moduleDirectories,
moduleFileExtensions: options.moduleFileExtensions,
moduleLoader: options.moduleLoader,
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-config/src/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,8 @@ export default function normalize(options: InitialOptions, argv: Argv) {
case 'listTests':
case 'logHeapUsage':
case 'mapCoverage':
case 'module':
case 'moduleDirectories':
case 'moduleFileExtensions':
case 'name':
case 'noStackTrace':
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/valid_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default ({
json: false,
lastCommit: false,
logHeapUsage: true,
module: false,
moduleDirectories: ['node_modules'],
moduleFileExtensions: ['js', 'json', 'jsx', 'node'],
moduleLoader: '<rootDir>',
Expand Down
40 changes: 34 additions & 6 deletions packages/jest-resolve/src/default_resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type ResolverOptions = {|
basedir: Path,
browser?: boolean,
extensions?: Array<string>,
module?: boolean,
moduleDirectory?: Array<string>,
paths?: ?Array<Path>,
rootDir: ?Path,
Expand All @@ -29,6 +30,7 @@ export default function defaultResolver(
return resolve(path, {
basedir: options.basedir,
extensions: options.extensions,
module: options.module,
moduleDirectory: options.moduleDirectory,
paths: options.paths,
rootDir: options.rootDir,
Expand All @@ -47,6 +49,7 @@ import isBuiltinModule from './is_builtin_module';
import nodeModulesPaths from './node_modules_paths';

const REGEX_RELATIVE_IMPORT = /^(?:\.\.?(?:\/|$)|\/|([A-Za-z]:)?[\\\/])/;
const REQUIRED_BY_MODULE_FIELD_CACHE = {};

function resolveSync(target: Path, options: ResolverOptions): Path {
const basedir = options.basedir;
Expand Down Expand Up @@ -122,15 +125,25 @@ function resolveSync(target: Path, options: ResolverOptions): Path {
return undefined;
}

const pkgfile = path.join(name, 'package.json');
let pkgmain;
let pkgentry, isRequiredByModuleField;
try {
const body = fs.readFileSync(pkgfile, 'utf8');
pkgmain = JSON.parse(body).main;
const pkgfile = getPackageFile(name);

if (hasModuleField(pkgfile)) {
isRequiredByModuleField = true;
pkgentry = pkgfile.module;
} else {
pkgentry = pkgfile.main;
}
} catch (e) {}

if (pkgmain && pkgmain !== '.') {
const resolveTarget = path.resolve(name, pkgmain);
if (pkgentry && pkgentry !== '.') {
const resolveTarget = path.resolve(name, pkgentry);

if (isRequiredByModuleField) {
REQUIRED_BY_MODULE_FIELD_CACHE[name] = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sadly I reverted to keeping a local CACHE singleton here, if you have any ideas, please let me know as well!

}

const result = tryResolve(resolveTarget);
if (result) {
return result;
Expand All @@ -141,6 +154,21 @@ function resolveSync(target: Path, options: ResolverOptions): Path {
}
}

function hasModuleField(pkgfile: string): boolean {
return !!pkgfile.module;
}

function getPackageFile(file: Path): string {
const pkgfile = path.join(file, 'package.json');

return JSON.parse(fs.readFileSync(pkgfile, 'utf8'));
}

export function couldBeRequiredByModuleField(file: Path): boolean {
return !!Object.keys(REQUIRED_BY_MODULE_FIELD_CACHE)
.find(moduleFieldCache => file.startsWith(moduleFieldCache));
}

/*
* helper functions
*/
Expand Down
12 changes: 11 additions & 1 deletion packages/jest-resolve/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import fs from 'fs';
import path from 'path';
import nodeModulesPaths from './node_modules_paths';
import isBuiltinModule from './is_builtin_module';
import defaultResolver from './default_resolver.js';
import defaultResolver, {couldBeRequiredByModuleField} from './default_resolver.js';
import chalk from 'chalk';

type ResolverConfig = {|
browser?: boolean,
defaultPlatform: ?string,
extensions: Array<string>,
hasCoreModules: boolean,
module?: boolean,
moduleDirectories: Array<string>,
moduleNameMapper: ?Array<ModuleNameMapperConfig>,
modulePaths: Array<Path>,
Expand All @@ -35,6 +36,7 @@ type FindNodeModuleConfig = {|
basedir: Path,
browser?: boolean,
extensions?: Array<string>,
module?: boolean,
moduleDirectory?: Array<string>,
paths?: Array<Path>,
resolver?: ?Path,
Expand Down Expand Up @@ -74,6 +76,7 @@ class Resolver {
extensions: options.extensions,
hasCoreModules:
options.hasCoreModules === undefined ? true : options.hasCoreModules,
module: options.module,
moduleDirectories: options.moduleDirectories || ['node_modules'],
moduleNameMapper: options.moduleNameMapper,
modulePaths: options.modulePaths,
Expand All @@ -99,6 +102,7 @@ class Resolver {
basedir: options.basedir,
browser: options.browser,
extensions: options.extensions,
module: options.module,
moduleDirectory: options.moduleDirectory,
paths: paths ? (nodePaths || []).concat(paths) : nodePaths,
rootDir: options.rootDir,
Expand All @@ -107,6 +111,10 @@ class Resolver {
return null;
}

isRequiredByModuleField(path: Path): boolean {
return this._options.module && couldBeRequiredByModuleField(path);
}

resolveModule(
from: Path,
moduleName: string,
Expand Down Expand Up @@ -155,6 +163,7 @@ class Resolver {
basedir: dirname,
browser: this._options.browser,
extensions,
module: this._options.module,
moduleDirectory,
paths,
resolver: this._options.resolver,
Expand Down Expand Up @@ -344,6 +353,7 @@ class Resolver {
basedir: dirname,
browser: this._options.browser,
extensions,
module: this._options.module,
moduleDirectory,
paths,
resolver,
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-runtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ class Runtime {
defaultPlatform: config.haste.defaultPlatform,
extensions: config.moduleFileExtensions.map(extension => '.' + extension),
hasCoreModules: true,
module: config.module,
moduleDirectories: config.moduleDirectories,
moduleNameMapper: getModuleNameMapper(config),
modulePaths: config.modulePaths,
Expand Down Expand Up @@ -501,6 +502,7 @@ class Runtime {
this._currentlyExecutingModulePath = filename;
const origCurrExecutingManualMock = this._isCurrentlyExecutingManualMock;
this._isCurrentlyExecutingManualMock = filename;
const isRequiredByModuleField = this._config.module && this._resolver.isRequiredByModuleField(filename);

const dirname = path.dirname(filename);
localModule.children = [];
Expand Down
15 changes: 10 additions & 5 deletions packages/jest-runtime/src/script_transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export default class ScriptTransformer {
}
}

transformSource(filepath: Path, content: string, instrument: boolean) {
transformSource(filepath: Path, content: string, instrument: boolean, isRequiredByModuleField: boolean) {
const filename = this._getRealPath(filepath);
const transform = this._getTransformer(filename);
const cacheFilePath = this._getFileCachePath(filename, content, instrument);
Expand Down Expand Up @@ -214,7 +214,7 @@ export default class ScriptTransformer {
map: null,
};

if (transform && shouldCallTransform) {
if (isRequiredByModuleField || (transform && shouldCallTransform)) {
const processed = transform.process(content, filename, this._config, {
instrument,
returnSourceString: false,
Expand Down Expand Up @@ -283,16 +283,21 @@ export default class ScriptTransformer {
let mapCoverage = false;

const willTransform =
!isInternalModule &&
!isCoreModule &&
(shouldTransform(filename, this._config) || instrument);
options.isRequiredByModuleField ||
(
!isInternalModule &&
!isCoreModule &&
(shouldTransform(filename, this._config) || instrument)
);

try {
if (willTransform) {
const transformedSource = this.transformSource(
filename,
content,
instrument,
options.isRequiredByModuleField,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For some reason, shouldTransform is called twice, I just passed it through for now. Maybe someone from the jest team can check if it is actually necessary to call it twice.

Copy link
Member

Choose a reason for hiding this comment

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

Twice within the same file or same code path?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same code path.

!!(options && options.mapCoverage),
);

wrappedCode = wrap(transformedSource.code);
Expand Down
7 changes: 5 additions & 2 deletions types/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,14 @@ export type DefaultOptions = {|
clearMocks: boolean,
coveragePathIgnorePatterns: Array<string>,
coverageReporters: Array<string>,
detectLeaks: boolean,
expand: boolean,
forceCoverageMatch: Array<Glob>,
globals: ConfigGlobals,
globalSetup: ?string,
globalTeardown: ?string,
haste: HasteConfig,
detectLeaks: boolean,
module: boolean,
moduleDirectories: Array<string>,
moduleFileExtensions: Array<string>,
moduleNameMapper: {[key: string]: string},
Expand Down Expand Up @@ -98,10 +99,11 @@ export type InitialOptions = {
globalTeardown?: ?string,
haste?: HasteConfig,
reporters?: Array<ReporterConfig | string>,
logHeapUsage?: boolean,
lastCommit?: boolean,
logHeapUsage?: boolean,
listTests?: boolean,
mapCoverage?: boolean,
module?: boolean,
moduleDirectories?: Array<string>,
moduleFileExtensions?: Array<string>,
moduleLoader?: Path,
Expand Down Expand Up @@ -224,6 +226,7 @@ export type ProjectConfig = {|
forceCoverageMatch: Array<Glob>,
globals: ConfigGlobals,
haste: HasteConfig,
module?: boolean,
moduleDirectories: Array<string>,
moduleFileExtensions: Array<string>,
moduleLoader: Path,
Expand Down