Skip to content

Commit

Permalink
Resolve relative paths relative to the module under test (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwalton authored and bendrucker committed Mar 2, 2018
1 parent 99dad5c commit c375628
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 17 deletions.
66 changes: 49 additions & 17 deletions lib/proxyquire.js
Expand Up @@ -2,8 +2,8 @@
/* jshint laxbreak:true, loopfunc:true */

var Module = require('module')
var path = require('path')
var resolve = require('resolve')
var dirname = require('path').dirname
var ProxyquireError = require('./proxyquire-error')
var is = require('./is')
var assert = require('assert')
Expand Down Expand Up @@ -129,13 +129,47 @@ Proxyquire.prototype.load = function (request, stubs) {
return this._withoutCache(this._parent, stubs, request, this._parent.require.bind(this._parent, request))
}

// Resolves a stub relative to a module.
// `baseModule` is the module we're resolving from. `pathToResolve` is the
// module we want to resolve (i.e. the string passed to `require()`).
Proxyquire.prototype._resolveModule = function (baseModule, pathToResolve) {
try {
return resolve.sync(pathToResolve, {
basedir: path.dirname(baseModule),
extensions: Object.keys(require.extensions),
paths: Module.globalPaths
})
} catch (err) {
// If this is not a relative path (e.g. "foo" as opposed to "./foo"), and
// we couldn't resolve it, then we just let the path through unchanged.
// It's safe to do this, because if two different modules require "foo",
// they both expect to get back the same thing.
if (pathToResolve[0] !== '.') {
return pathToResolve
}

// If `pathToResolve` is relative, then it is *not* safe to return it,
// since a file in one directory that requires "./foo" expects to get
// back a different module than one that requires "./foo" from another
// directory. However, if !this._preserveCache, then we don't want to
// throw, since we can resolve modules that don't exist. Resolve as
// best we can.
if (!this._preserveCache) {
return path.resolve(path.dirname(baseModule), pathToResolve)
}

throw err
}
}

// This replaces a module's require function
Proxyquire.prototype._require = function (module, stubs, path) {
assert(typeof path === 'string', 'path must be a string')
assert(path, 'missing path')

if (hasOwnProperty.call(stubs, path)) {
var stub = stubs[path]
var resolvedPath = this._resolveModule(module.filename, path)
if (hasOwnProperty.call(stubs, resolvedPath)) {
var stub = stubs[resolvedPath]

if (stub === null) {
// Mimic the module-not-found exception thrown by node.js.
Expand Down Expand Up @@ -163,6 +197,15 @@ Proxyquire.prototype._require = function (module, stubs, path) {
Proxyquire.prototype._withoutCache = function (module, stubs, path, func) {
// Temporarily disable the cache - either per-module or globally if we have global stubs
var restoreCache = this._disableCache(module, path)
var resolvedPath = Module._resolveFilename(path, module)

// Resolve all stubs to absolute paths.
stubs = Object.keys(stubs)
.reduce(function (result, stubPath) {
var resolvedStubPath = this._resolveModule(resolvedPath, stubPath)
result[resolvedStubPath] = stubs[stubPath]
return result
}.bind(this), {})

// Override all require extension handlers
var restoreExtensionHandlers = this._overrideExtensionHandlers(module, stubs)
Expand All @@ -175,18 +218,7 @@ Proxyquire.prototype._withoutCache = function (module, stubs, path, func) {
if (this._preserveCache) {
restoreCache()
} else {
var id = Module._resolveFilename(path, module)
var stubIds = Object.keys(stubs).map(function (stubPath) {
try {
return resolve.sync(stubPath, {
basedir: dirname(id),
extensions: Object.keys(require.extensions),
paths: Module.globalPaths
})
} catch (_) {}
})
var ids = [id].concat(stubIds.filter(Boolean))

var ids = [resolvedPath].concat(Object.keys(stubs).filter(Boolean))
ids.forEach(function (id) {
delete require.cache[id]
})
Expand Down Expand Up @@ -251,7 +283,7 @@ Proxyquire.prototype._disableModuleCache = function (path, module) {
}
}

Proxyquire.prototype._overrideExtensionHandlers = function (module, stubs) {
Proxyquire.prototype._overrideExtensionHandlers = function (module, resolvedStubs) {
/* eslint node/no-deprecated-api: [error, {ignoreGlobalItems: ["require.extensions"]}] */

var originalExtensions = {}
Expand All @@ -266,7 +298,7 @@ Proxyquire.prototype._overrideExtensionHandlers = function (module, stubs) {
// Override the default handler for the requested file extension
require.extensions[extension] = function (module, filename) {
// Override the require method for this module
module.require = self._require.bind(self, module, stubs)
module.require = self._require.bind(self, module, resolvedStubs)

return originalExtensions[extension](module, filename)
}
Expand Down
13 changes: 13 additions & 0 deletions test/proxyquire-relative-paths.js
@@ -0,0 +1,13 @@
'use strict'

/* jshint asi:true */
/* global describe, it */

var proxyquire = require('..')

describe('When requiring relative paths, they should be relative to the proxyrequired module', function () {
it('should return the correct result', function () {
var result = proxyquire('./samples/relative-paths/a/index.js', {'./util': {c: 'c'}})
result.should.eql({a: 'a', c: 'c'})
})
})
4 changes: 4 additions & 0 deletions test/samples/relative-paths/a/index.js
@@ -0,0 +1,4 @@
var util = require('./util')
require('../b')

module.exports = util
1 change: 1 addition & 0 deletions test/samples/relative-paths/a/util.js
@@ -0,0 +1 @@
exports.a = 'a'
1 change: 1 addition & 0 deletions test/samples/relative-paths/b/index.js
@@ -0,0 +1 @@
require('./util')
1 change: 1 addition & 0 deletions test/samples/relative-paths/b/util.js
@@ -0,0 +1 @@
exports.b = 'b'

0 comments on commit c375628

Please sign in to comment.