Skip to content

Commit e0d6c57

Browse files
committed
fix(import): wrong error message when a module exists but fails
The importer is responsible of importing and patching modules with configurable fallbacks. If one is missing, it goes to the next one. Previously if a module existed but was failing when loading it with `require`, it'd report as missing instead of failing. Now it bubbles the error correctly.
1 parent e765875 commit e0d6c57

File tree

5 files changed

+143
-39
lines changed

5 files changed

+143
-39
lines changed

src/config/config-set.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export class ConfigSet {
354354
let hooksFile = process.env.TS_JEST_HOOKS
355355
if (hooksFile) {
356356
hooksFile = resolve(this.cwd, hooksFile)
357-
return importer.tryThese(hooksFile) || {}
357+
return importer.tryTheseOr(hooksFile, {})
358358
}
359359
return {}
360360
}

src/types.ts

-7
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,6 @@ export interface CreateJestPresetOptions {
126126
*/
127127
export type ModulePatcher<T = any> = (module: T) => T
128128

129-
/**
130-
* @internal
131-
*/
132-
export interface TsJestImporter {
133-
tryThese(moduleName: string, ...fallbacks: string[]): any
134-
}
135-
136129
/**
137130
* Common TypeScript interfaces between versions.
138131
*/

src/util/importer.spec.ts

+65-18
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,21 @@ import * as fakers from '../__helpers__/fakers'
33

44
import { Importer, __requireModule } from './importer'
55

6-
const requireModule = jest.fn(
7-
mod =>
8-
mod in modules
9-
? modules[mod]()
10-
: (() => {
11-
const err: any = new Error(`Module not found: ${mod}.`)
12-
err.code = 'MODULE_NOT_FOUND'
13-
throw err
14-
})(),
15-
)
16-
__requireModule(requireModule as any)
6+
const moduleNotFound = (mod: string) => {
7+
const err: any = new Error(`Module not found: ${mod}.`)
8+
err.code = 'MODULE_NOT_FOUND'
9+
throw err
10+
}
11+
const fakeFullPath = (mod: string) => `/root/${mod}.js`
12+
const requireModule = jest.fn(mod => (mod in modules ? modules[mod]() : moduleNotFound(mod)))
13+
const resolveModule = jest.fn(mod => (mod in modules ? fakeFullPath(mod) : moduleNotFound(mod)))
14+
__requireModule(requireModule as any, resolveModule)
1715

1816
let modules!: { [key: string]: () => any }
1917
beforeEach(() => {
2018
modules = {}
2119
requireModule.mockClear()
20+
resolveModule.mockClear()
2221
})
2322

2423
describe('instance', () => {
@@ -30,12 +29,60 @@ describe('instance', () => {
3029
})
3130
})
3231

33-
describe('tryTheese', () => {
34-
it('tries until it find one not failing', () => {
32+
describe('tryThese', () => {
33+
it('should try until it finds one existing', () => {
3534
modules = {
36-
success: () => 'success',
35+
success: () => 'ok',
3736
}
38-
expect(new Importer().tryThese('fail1', 'fail2', 'success')).toBe('success')
37+
expect(new Importer().tryThese('missing1', 'missing2', 'success')).toMatchInlineSnapshot(`
38+
Object {
39+
"exists": true,
40+
"exports": "ok",
41+
"given": "success",
42+
"path": "/root/success.js",
43+
}
44+
`)
45+
})
46+
it('should return the error when one is failing', () => {
47+
modules = {
48+
fail1: () => {
49+
throw new Error('foo')
50+
},
51+
success: () => 'ok',
52+
}
53+
const res = new Importer().tryThese('missing1', 'fail1', 'success')
54+
expect(res).toMatchObject({
55+
exists: true,
56+
error: expect.any(Error),
57+
given: 'fail1',
58+
path: fakeFullPath('fail1'),
59+
})
60+
expect(res).not.toHaveProperty('exports')
61+
expect((res as any).error.message).toMatch(/\bfoo\b/)
62+
})
63+
})
64+
65+
describe('tryTheseOr', () => {
66+
it('should try until it find one not failing', () => {
67+
expect(new Importer().tryTheseOr(['fail1', 'fail2', 'success'])).toBeUndefined()
68+
expect(new Importer().tryTheseOr(['fail1', 'fail2', 'success'], 'foo')).toBe('foo')
69+
modules = {
70+
success: () => 'ok',
71+
}
72+
expect(new Importer().tryTheseOr(['fail1', 'fail2', 'success'])).toBe('ok')
73+
modules.fail2 = () => {
74+
throw new Error('foo')
75+
}
76+
expect(new Importer().tryTheseOr(['fail1', 'fail2', 'success'], 'bar', true)).toBe('bar')
77+
})
78+
it('should fail if one is throwing', () => {
79+
modules = {
80+
success: () => 'ok',
81+
fail2: () => {
82+
throw new Error('foo')
83+
},
84+
}
85+
expect(() => new Importer().tryTheseOr(['fail1', 'fail2', 'success'], 'bar')).toThrow(/\bfoo\b/)
3986
})
4087
})
4188

@@ -49,10 +96,10 @@ describe('patcher', () => {
4996
foo: () => ({ foo: true }),
5097
bar: () => ({ bar: true }),
5198
}
52-
expect(imp.tryThese('foo')).toEqual({ foo: true, p1: true, p2: true })
53-
expect(imp.tryThese('foo')).toEqual({ foo: true, p1: true, p2: true })
99+
expect(imp.tryTheseOr('foo')).toEqual({ foo: true, p1: true, p2: true })
100+
expect(imp.tryTheseOr('foo')).toEqual({ foo: true, p1: true, p2: true })
54101

55-
expect(imp.tryThese('bar')).toEqual({ bar: true })
102+
expect(imp.tryTheseOr('bar')).toEqual({ bar: true })
56103

57104
// ensure cache has been used
58105
expect(patch1).toHaveBeenCalledTimes(1)

src/util/importer.ts

+76-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ModulePatcher, TBabelCore, TBabelJest, TTypeScript, TsJestImporter } from '../types'
1+
import { ModulePatcher, TBabelCore, TBabelJest, TTypeScript } from '../types'
22

33
import * as hacks from './hacks'
44
import { rootLogger } from './logger'
@@ -26,7 +26,7 @@ const passThru = (action: () => void) => (input: any) => {
2626
/**
2727
* @internal
2828
*/
29-
export class Importer implements TsJestImporter {
29+
export class Importer {
3030
@Memoize()
3131
static get instance() {
3232
logger.debug('creating Importer singleton')
@@ -70,22 +70,51 @@ export class Importer implements TsJestImporter {
7070
}
7171

7272
@Memoize((...args: string[]) => args.join(':'))
73-
tryThese(moduleName: string, ...fallbacks: string[]): any {
73+
tryThese(moduleName: string, ...fallbacks: string[]) {
7474
let name: string
75-
let loaded: any
75+
let loaded: RequireResult<true> | undefined
7676
const tries = [moduleName, ...fallbacks]
7777
// tslint:disable-next-line:no-conditional-assignment
7878
while ((name = tries.shift() as string) !== undefined) {
79-
try {
80-
loaded = requireModule(name)
81-
logger.debug('loaded module', name)
79+
const req = requireWrapper(name)
80+
81+
// remove exports from what we're going to log
82+
const contextReq: any = { ...req }
83+
delete contextReq.exports
84+
85+
if (req.exists) {
86+
// module exists
87+
loaded = req as RequireResult<true>
88+
if (loaded.error) {
89+
// require-ing it failed
90+
logger.error({ requireResult: contextReq }, `failed loading module '${name}'`, loaded.error.message)
91+
} else {
92+
// it has been loaded, let's patch it
93+
logger.debug({ requireResult: contextReq }, 'loaded module', name)
94+
loaded.exports = this._patch(name, loaded.exports)
95+
}
8296
break
83-
} catch (err) {
84-
logger.debug('fail loading module', name)
97+
} else {
98+
// module does not exists in the path
99+
logger.debug({ requireResult: contextReq }, `module '${name}' not found`)
100+
continue
85101
}
86102
}
87103

88-
return loaded && this._patch(name, loaded)
104+
// return the loaded one, could be one that has been loaded, or one which has failed during load
105+
// but not one which does not exists
106+
return loaded
107+
}
108+
109+
tryTheseOr<T>(moduleNames: [string, ...string[]] | string, missingResult: T, allowLoadError?: boolean): T
110+
tryTheseOr<T>(moduleNames: [string, ...string[]] | string, missingResult?: T, allowLoadError?: boolean): T | undefined
111+
tryTheseOr<T>(moduleNames: [string, ...string[]] | string, missingResult?: T, allowLoadError = false): T | undefined {
112+
const args: [string, ...string[]] = Array.isArray(moduleNames) ? moduleNames : [moduleNames]
113+
const result = this.tryThese(...args)
114+
if (!result) return missingResult
115+
if (!result.error) return result.exports as T
116+
if (allowLoadError) return missingResult
117+
throw result.error
89118
}
90119

91120
@Memoize(name => name)
@@ -105,8 +134,10 @@ export class Importer implements TsJestImporter {
105134
// try to load any of the alternative after trying main one
106135
const res = this.tryThese(moduleName, ...alternatives)
107136
// if we could load one, return it
108-
if (res) {
109-
return res
137+
if (res && res.exists) {
138+
if (!res.error) return res.exports
139+
// it could not load because of a failure while importing, but it exists
140+
throw new Error(interpolate(Errors.LoadingModuleFailed, { module: res.given, error: res.error.message }))
110141
}
111142

112143
// if it couldn't load, build a nice error message so the user can fix it by himself
@@ -136,11 +167,43 @@ export class Importer implements TsJestImporter {
136167
*/
137168
export const importer = Importer.instance
138169

170+
/**
171+
* @internal
172+
*/
173+
export interface RequireResult<E = boolean> {
174+
exists: E
175+
given: string
176+
path?: string
177+
exports?: any
178+
error?: Error
179+
}
180+
181+
function requireWrapper(moduleName: string): RequireResult {
182+
let path: string
183+
let exists = false
184+
try {
185+
path = resolveModule(moduleName)
186+
exists = true
187+
} catch (error) {
188+
return { error, exists, given: moduleName }
189+
}
190+
const result: RequireResult = { exists, path, given: moduleName }
191+
try {
192+
result.exports = requireModule(moduleName)
193+
} catch (error) {
194+
result.error = error
195+
}
196+
return result
197+
}
198+
139199
let requireModule = (mod: string) => require(mod)
200+
let resolveModule = (mod: string) => require.resolve(mod)
201+
140202
/**
141203
* @internal
142204
*/
143205
// so that we can test easier
144-
export function __requireModule(localRequire: typeof requireModule) {
206+
export function __requireModule(localRequire: typeof requireModule, localResolve: typeof resolveModule) {
145207
requireModule = localRequire
208+
resolveModule = localResolve
146209
}

src/util/messages.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* @internal
55
*/
66
export enum Errors {
7+
LoadingModuleFailed = 'Loading module {{module}} failed with error: {{error}}',
78
UnableToLoadOneModule = 'Unable to load the module {{module}}. {{reason}} To fix it:\n{{fix}}',
89
UnableToLoadAnyModule = 'Unable to load any of these modules: {{module}}. {{reason}}. To fix it:\n{{fix}}',
910
TypesUnavailableWithoutTypeCheck = 'Type information is unavailable with "isolatedModules"',

0 commit comments

Comments
 (0)