1+ /* eslint-disable no-underscore-dangle */
2+ /* eslint-disable @typescript-eslint/no-var-requires */
3+ /* eslint-disable @typescript-eslint/no-require-imports */
14import path from 'path' ;
25import callsites from 'callsites' ;
3- import { bold , green , grey } from './lib/colors' ;
4- // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
6+ import { bold , green , gray } from 'colorette' ;
57const Module = require ( 'module' ) ;
6- const registeredMocks = new Map < string , { modulePath : string , mockReturnValue : any } > ( ) ;
8+
9+ type MockedDeps = {
10+ dependencyPath : string // the dependency being mocked
11+ mockReturnValue : any // what to return for that dependency
12+ parentPath : string // the module whose requires we're intercepting
13+ } ;
14+
15+ const depsToMock = new Map < string , MockedDeps > ( ) ;
716
817function debug ( msg : any ) {
918
@@ -19,93 +28,96 @@ Module.prototype.require = new Proxy(Module.prototype.require, {
1928 const [ name ] = argumentsList ;
2029 // eslint-disable-next-line no-underscore-dangle
2130 const absolutePath = Module . _resolveFilename ( name , thisArg ) ;
22- const mock = registeredMocks . get ( absolutePath ) ;
31+ const mock = depsToMock . get ( absolutePath ) ;
2332
24- if ( mock ) {
33+ // Only replace if the module is a direct dependency of the caller
34+ if ( mock && mock . parentPath === thisArg . filename ) {
2535
2636 debug ( `require(): ${ green ( 'REPLACING WITH MOCK' ) } ${ bold ( name ) } [${ absolutePath } ] ${ getStackTrace ( ) } ` ) ;
27- registeredMocks . delete ( absolutePath ) ;
37+ depsToMock . delete ( absolutePath ) ;
2838 return mock . mockReturnValue ;
2939
30- } else {
31-
32- debug ( `require(): ${ bold ( name ) } [${ absolutePath } ] ${ getStackTrace ( ) } ` ) ;
33-
3440 }
3541
42+ debug ( `require(): ${ bold ( name ) } [${ absolutePath } ] ${ getStackTrace ( ) } ` ) ;
3643 return Reflect . apply ( target , thisArg , argumentsList ) ;
3744
3845 } ,
3946} ) ;
4047
41- function resolve ( modulePath : string , dir : string , parentModule : any ) : string {
48+ /**
49+ * Resolves a module path to an absolute path
50+ * @param modulePath - The module path to resolve
51+ * @param dir - The directory to resolve the module path in
52+ * @param parentModule - The parent module to resolve the module path in
53+ * @returns The absolute path of the module
54+ */
55+ function resolve (
56+ modulePath : string ,
57+ dir : string ,
58+ parentModule : NodeJS . Module | null | undefined
59+ ) : string {
4260
4361 // if path starts with ., then it's relative
4462 if ( modulePath . slice ( 0 , 1 ) === '.' ) {
4563
4664 const resolvedAbsPath = path . resolve ( dir , modulePath ) ;
4765
48- // eslint-disable-next-line no-underscore-dangle
4966 return Module . _resolveFilename ( resolvedAbsPath , parentModule ) ;
5067
5168 }
5269
53- // eslint-disable-next-line no-underscore-dangle
5470 return Module . _resolveFilename ( modulePath , parentModule ) ;
5571
5672}
5773
58- function registerMockModules ( mockModules : any , dir : string , parentModule : any ) {
74+ function registerDepsToReplace (
75+ mockModules : any ,
76+ dir : string ,
77+ parentModule : NodeJS . Module | null | undefined ,
78+ targetModulePath : string
79+ ) {
5980
6081 Object . entries ( mockModules ) . forEach ( ( mockModule : any ) => {
6182
6283 const [ modulePath , mockReturnValue ] = mockModule ;
6384 const absolutePath = resolve ( modulePath , dir , parentModule ) ;
6485
65- debug ( `registerMocks(): ${ modulePath } [${ absolutePath } ]` ) ;
66-
67- if ( ! absolutePath ) {
68-
69- throw new Error ( `Unable to find module "${ modulePath } ".` ) ;
86+ debug ( `will replace: ${ modulePath } [${ absolutePath } ]` ) ;
7087
71- }
72-
73- registeredMocks . set ( absolutePath , {
74- modulePath,
88+ depsToMock . set ( absolutePath , {
89+ dependencyPath : modulePath ,
7590 mockReturnValue,
91+ parentPath : targetModulePath , // This is the module being mocked
7692 } ) ;
7793
7894 } ) ;
7995
8096}
8197
82- export function mock ( modulePath : string , mocks : Record < string , any > = { } ) {
98+ export function mock ( modulePath : string , deps : Record < string , any > = { } ) {
8399
84100 const callerFile = callsites ( ) [ 1 ] . getFileName ( ) as string ;
85- const parentModule = module . parent ?. parent ;
101+ const callerModule = Object . values ( Module . _cache ) . find ( ( mod : any ) => mod . filename === callerFile ) as NodeJS . Module
102+ ?? module . parent ?. parent ; // this fallback assumes the caller is two levels up (mock.ts -> index.ts -> caller)
86103 const dir = path . dirname ( callerFile ) ;
87- const absolutePath = resolve ( modulePath , dir , parentModule ) ;
104+ const absolutePath = resolve ( modulePath , dir , callerModule ) ;
88105 const moduleDir = path . dirname ( absolutePath ) ;
89106
90- debug ( `mock(): ${ modulePath } [${ absolutePath } ]` ) ;
91-
92- if ( ! absolutePath ) {
93-
94- throw new Error ( `Unable to find ${ modulePath } ` ) ;
95-
96- }
107+ debug ( `mocking: ${ modulePath } [${ absolutePath } ]` ) ;
97108
98- registerMockModules ( mocks , moduleDir , parentModule ) ;
109+ // Pass the absolutePath as the targetModulePath
110+ registerDepsToReplace ( deps , moduleDir , callerModule , absolutePath ) ;
99111 delete require . cache [ absolutePath ] ;
100112
101- // eslint-disable-next-line @typescript-eslint/no- require-imports,@typescript-eslint/no-var-requires
113+ // require the module that we're mocking
102114 const mod = require ( absolutePath ) ;
103115
104116 // make sure there are no unused mocks
105- if ( registeredMocks . size ) {
117+ if ( depsToMock . size ) {
106118
107119 throw new Error ( `The following imports were not found in ${ modulePath } :
108- ${ [ ...registeredMocks . values ( ) ] . map ( ( mock ) => mock . modulePath ) . join ( ', ' ) } ` ) ;
120+ ${ [ ...depsToMock . values ( ) ] . map ( ( mock ) => mock . dependencyPath ) . join ( ', ' ) } ` ) ;
109121
110122 }
111123
@@ -124,13 +136,14 @@ function getStackTrace() {
124136
125137 const file = callsite . getFileName ( ) ;
126138
139+ // filter out internal, node_modules, and cjs-mock files
127140 return file
128- && ! file . includes ( 'internal' )
129- && ! file . includes ( 'node_modules' )
130- && ! file . includes ( 'cjs-mock' ) ;
141+ && ! file . includes ( 'internal' )
142+ && ! file . includes ( 'node_modules' )
143+ && ! file . includes ( 'cjs-mock' ) ;
131144
132145 } )
133- . map ( ( callsite ) => grey ( ` at ${ callsite . getFileName ( ) } ${ callsite . getLineNumber ( ) } :${ callsite . getColumnNumber ( ) } ` ) )
146+ . map ( ( callsite ) => gray ( ` at ${ callsite . getFileName ( ) } ${ callsite . getLineNumber ( ) } :${ callsite . getColumnNumber ( ) } ` ) )
134147 . join ( '\n' ) ;
135148
136149 return trace ? `\n${ trace } ` : '' ;
0 commit comments