Skip to content

Commit

Permalink
feat: add require stack & surface Yarn PnP (jestjs#9681)
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Scabbiolo authored Mar 25, 2020
1 parent 553b788 commit cd98198
Show file tree
Hide file tree
Showing 17 changed files with 223 additions and 42 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest-runtime]` Require stack when a module cannot be resolved ([#9681](https://github.com/facebook/jest/pull/9681))
- `[jest-config]` Support ESM config files with `.js` extension ([#9573](https://github.com/facebook/jest/pull/9573)).
- `[jest-runtime]` Override `module.createRequire` to return a Jest-compatible `require` function ([#9469](https://github.com/facebook/jest/pull/9469))
- `[jest-haste-map]` [**BREAKING**] Remove `mapper` option ([#9581](https://github.com/facebook/jest/pull/9581))
Expand All @@ -11,6 +12,7 @@

### Fixes

- `[jest-runtime]` Yarn PnP errors displayed to the user ([#9681](https://github.com/facebook/jest/pull/9681))
- `[expect]` Handle readonly properties correctly ([#9575](https://github.com/facebook/jest/pull/9575))
- `[jest-cli]` Set `coverageProvider` correctly when provided in config ([#9562](https://github.com/facebook/jest/pull/9562))
- `[jest-cli]` Allow specifying `.cjs` and `.mjs` config files by `--config` CLI option ([#9578](https://github.com/facebook/jest/pull/9578))
Expand Down
4 changes: 2 additions & 2 deletions e2e/__tests__/__snapshots__/moduleNameMapper.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:519:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17)
at Object.require (index.js:10:1)
`;

Expand Down Expand Up @@ -65,6 +65,6 @@ FAIL __tests__/index.js
12 | module.exports = () => 'test';
13 |
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:519:17)
at createNoMappedModuleFoundError (../../packages/jest-resolve/build/index.js:540:17)
at Object.require (index.js:10:1)
`;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ FAIL __tests__/test.js
Cannot find module './some-json-file' from 'index.js'
Require stack:
index.js
__tests__/test.js
However, Jest was able to find:
'./some-json-file.json'
Expand All @@ -32,6 +37,6 @@ FAIL __tests__/test.js
| ^
9 |
at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:276:11)
at Resolver.resolveModule (../../packages/jest-resolve/build/index.js:296:11)
at Object.require (index.js:8:18)
`;
2 changes: 1 addition & 1 deletion e2e/__tests__/pnp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ it('sucessfully runs the tests inside `pnp/`', () => {
nodeOptions: `--require ${DIR}/.pnp.js`,
});
expect(json.success).toBe(true);
expect(json.numTotalTestSuites).toBe(1);
expect(json.numTotalTestSuites).toBe(2);
});
13 changes: 13 additions & 0 deletions e2e/pnp/__tests__/undeclared-dependency.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* 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.
*
*/

it('should surface pnp errors', () => {
expect(() => {
require('undeclared');
}).toThrow(expect.objectContaining({code: 'UNDECLARED_DEPENDENCY'}));
});
3 changes: 2 additions & 1 deletion e2e/pnp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"dependencies": {
"foo": "link:./lib"
"foo": "link:./lib",
"undeclared": "link:./undeclared-dependency"
},
"installConfig": {
"pnp": true
Expand Down
11 changes: 11 additions & 0 deletions e2e/pnp/undeclared-dependency/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* 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.
*
*/

const nope = require('unesitent_module__');

module.exports = () => nope;
3 changes: 3 additions & 0 deletions e2e/pnp/undeclared-dependency/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": "0.0.0"
}
4 changes: 4 additions & 0 deletions e2e/pnp/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
"foo@link:./lib":
version "0.0.0"
uid ""

"undeclared@link:./undeclared-dependency":
version "0.0.0"
uid ""
12 changes: 12 additions & 0 deletions e2e/resolve/Test7.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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.
*
*/

require('./test1');
const requiresUnexistingModule = require('./requiresUnexistingModule');

module.exports = {module: requiresUnexistingModule};
Empty file added e2e/resolve/__tests__/nope.txt
Empty file.
22 changes: 20 additions & 2 deletions e2e/resolve/__tests__/resolve.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/
'use strict';

const dedent = require('dedent');

let platform;

function testRequire(filename) {
Expand Down Expand Up @@ -99,11 +101,27 @@ test('should require resolve haste mocks correctly', () => {
expect(require('Test6').key).toBe('mock');
});

test('should throw module not found error if the module cannot be found', () => {
test('should throw module not found error if the module has dependencies that cannot be found', () => {
expect(() => require('Test7')).toThrow(
expect.objectContaining({
code: 'MODULE_NOT_FOUND',
message: "Cannot find module 'Test7' from 'resolve.test.js'",
message: dedent`
Cannot find module 'nope' from 'requiresUnexistingModule.js'
Require stack:
requiresUnexistingModule.js
Test7.js
__tests__/resolve.test.js\n
`,
})
);
});

test('should throw module not found error if the module cannot be found', () => {
expect(() => require('Test8')).toThrow(
expect.objectContaining({
code: 'MODULE_NOT_FOUND',
message: "Cannot find module 'Test8' from 'resolve.test.js'",
})
);
});
12 changes: 12 additions & 0 deletions e2e/resolve/requiresUnexistingModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 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.
*
*/

require('./test2');
const unexistedModule = require('nope');

module.exports = {module: unexistedModule};
6 changes: 4 additions & 2 deletions packages/jest-reporters/src/__tests__/notify_reporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import type {AggregatedResult} from '@jest/test-result';
import type {Config} from '@jest/types';
import Resolver from 'jest-resolve';
import NotifyReporter from '../notify_reporter';
import {makeGlobalConfig} from '../../../../TestUtils';

Expand Down Expand Up @@ -223,8 +224,9 @@ describe('node-notifier is an optional dependency', () => {

test('without node-notifier uses mock function that throws an error', () => {
jest.doMock('node-notifier', () => {
const error: any = new Error("Cannot find module 'node-notifier'");
error.code = 'MODULE_NOT_FOUND';
const error: any = new Resolver.ModuleNotFoundError(
"Cannot find module 'node-notifier'",
);
throw error;
});

Expand Down
46 changes: 46 additions & 0 deletions packages/jest-resolve/src/ModuleNotFoundError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@
* LICENSE file in the root directory of this source tree.
*/

import * as path from 'path';
import type {Config} from '@jest/types';
import slash = require('slash');

export default class ModuleNotFoundError extends Error {
code = 'MODULE_NOT_FOUND';
public hint?: string;
public requireStack?: Array<Config.Path>;
public siblingWithSimilarExtensionFound?: boolean;
public moduleName?: string;

private _originalMessage?: string;

constructor(message: string, moduleName?: string) {
super(message);
this._originalMessage = message;
this.moduleName = moduleName;
}

public buildMessage(rootDir: Config.Path): void {
if (!this._originalMessage) {
this._originalMessage = this.message || '';
}

let message = this._originalMessage;

if (this?.requireStack?.length && this!.requireStack!.length > 1) {
message += `
Require stack:
${(this.requireStack as Array<string>)
.map(p => p.replace(`${rootDir}${path.sep}`, ''))
.map(slash)
.join('\n ')}
`;
}

if (this.hint) {
message += this.hint;
}

this.message = message;
}

public static duckType(error: ModuleNotFoundError): ModuleNotFoundError {
error.buildMessage = ModuleNotFoundError.prototype.buildMessage;
return error;
}
}
29 changes: 26 additions & 3 deletions packages/jest-resolve/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type FindNodeModuleConfig = {
paths?: Array<Config.Path>;
resolver?: Config.Path | null;
rootDir?: Config.Path;
throwIfNotFound?: boolean;
};

type BooleanObject = Record<string, boolean>;
Expand Down Expand Up @@ -82,6 +83,21 @@ class Resolver {

static ModuleNotFoundError = ModuleNotFoundError;

static tryCastModuleNotFoundError(
error: unknown,
): ModuleNotFoundError | null {
if (error instanceof ModuleNotFoundError) {
return error as ModuleNotFoundError;
}

const casted = error as ModuleNotFoundError;
if (casted.code === 'MODULE_NOT_FOUND') {
return ModuleNotFoundError.duckType(casted);
}

return null;
}

static clearDefaultResolverCache(): void {
clearDefaultResolverCache();
}
Expand All @@ -105,7 +121,11 @@ class Resolver {
paths: paths ? (nodePaths || []).concat(paths) : nodePaths,
rootDir: options.rootDir,
});
} catch (e) {}
} catch (e) {
if (options.throwIfNotFound) {
throw e;
}
}
return null;
}

Expand Down Expand Up @@ -155,7 +175,7 @@ class Resolver {
const skipResolution =
options && options.skipNodeResolution && !moduleName.includes(path.sep);

const resolveNodeModule = (name: Config.Path) =>
const resolveNodeModule = (name: Config.Path, throwIfNotFound = false) =>
Resolver.findNodeModule(name, {
basedir: dirname,
browser: this._options.browser,
Expand All @@ -164,10 +184,12 @@ class Resolver {
paths,
resolver: this._options.resolver,
rootDir: this._options.rootDir,
throwIfNotFound,
});

if (!skipResolution) {
module = resolveNodeModule(moduleName);
// @ts-ignore: the "pnp" version named isn't in DefinitelyTyped
module = resolveNodeModule(moduleName, Boolean(process.versions.pnp));

if (module) {
this._moduleNameCache.set(key, module);
Expand Down Expand Up @@ -215,6 +237,7 @@ class Resolver {

throw new ModuleNotFoundError(
`Cannot find module '${moduleName}' from '${relativePath || '.'}'`,
moduleName,
);
}

Expand Down
Loading

0 comments on commit cd98198

Please sign in to comment.