Babel-plugin-module-resolver: Problems With Proxyquire

Created on 6 Dec 2017  路  18Comments  路  Source: tleunen/babel-plugin-module-resolver

Proxyquire is a library for replacing a module's dependencies (for testing). Unfortunately however it works based on paths, and module-resolver changes those paths in a way that _seems_ incompatible with proxyquire (eg. if you define an alias of ~/ then try to bring a module in with proxyquire using '~/some/path' it fails).

However, I don't fully understand how/when module-resolver is applied, so my hope is that someone more knowledgeable with the plug-in might be able to suggest a strategy for using the two libraries together (eg. by explaining what paths module-resolver generates so that one can use those paths with proxyquire). Failing that, just having a knowledgeable source confirm that it's impossible to use the two together would still be (at least somewhat) helpful.

Thanks.

Most helpful comment

Maybe it's best to let the end user define a function after all? And if we see later that there's a demand for something like this, we can revisit our options?

That's totally reasonable, but then would it maybe make sense to have a quick note about proxyquire/passthrough functions/transformFunctions somewhere in the docs? Even just a sentence or two (similar to the ESLint note) might help other proxyquire users.

All 18 comments

module-resolver looks at all the paths that do not start with . and then transforms them based on the configuration provided through babel config. Only paths that are in import-s or that appear in specific functions are transformed. The process happens when babel is running.

As far as I can see, proxyquire provides its own function for requiring the paths. Maybe your problem can be solved by adding said function to the list of handled functions. To do that, us the transformFunctions option in the plugin config.

Let us know if that helped!

@machineghost Have you tried to add the proxyquire function (if that's the name you use) as one of the transformed function in the plugin?

Thanks both for the responses, but unfortunately adding proxyquire to the transformedFunctions didn't help. Here's the relevant bits of my code, if it helps:

// .babelrc
// (was unclear whether I needed to include the default transformFunctions or not, so I did)
  "plugins": [
    ["module-resolver", {
      "alias": { "~": "./" },
      "transformFunctions": [
        "proxyquire",
        "require",
        "require.resolve",
        "System.import",
        "jest.genMockFromModule",
        "jest.mock",
        "jest.unmock",
        "jest.doMock",
        "jest.dontMock"
      ]
    }],

// src/some/path/foo.js
export const doFoo = () => { throw new Error('original')};

// src/some/path/bar.js
import { doFoo } from '~/src/some/path/bar';
export const doBar = () => doFoo();

// src/some/path/test.js
import proxyquire from 'proxyquire';
const { doBar } = proxyquire('~/src/some/path/bar', {
  '~/src/some/path/foo': { doFoo: () => { throw new Error('proxied') }}
});
doBar();

When I run the above code I get an original error, not a proxied error. However if I change the path of bar (but leave foo's path the same) from ~/src/some/path/bar to ../../some/path/bar I do get proxied as expected.

Quick update: if I change the path of bar in my test to ../../some/path/bar but leave foo.js referencing ~/src/some/path/bar, I also get the (desired) error of proxied. So it seems like whatever proxyquire does happens after the plug-in has re-written the path (to the ../ style), and making it a transformedFunction doesn't change that.

Honestly now that I can at least use proxyrequire and babel-plugin-module-resolver together one could consider this ticket "solved". However, if there is some other config option I could use to keep my proxyquire paths consistent that would be nice to know.

Hmm.. Just by curiosity, could you run babel on your test file only and see if the path gets transformed? You can do so by using the babel cli command.

The idea is to see if the path from proxyquire gets properly transformed

Here's the generated code (of the relevant part of test).

Original:

const { doBar } = proxyquire('~/src/some/path/bar', {
   '~/src/some/path/foo': { doFoo: () => { throw new Error('proxied') }}
});

Transformed:

  var _proxyquire = (0, _proxyquire3.default)('../../some/path/bar', {
    '~/some/path/foo':  { doFoo: function doFoo() {
        throw new Error('proxied');
      }}
  }),

EDIT: I think I might be losing info when I convert my real paths to the fake ones in my example; I'll repost with my actual code to make sure.

Yeah it was; I corrected my previous fake code. Here's the actual code version:

src/client/src/store/SheetLayout.js (foo):

export const buildLoadSheet = sheetId => { throw new Error('original')}

src/sheet/routes/loadSheetIfNeeded.js (bar):

export const mapDispatchToProps = (dispatch, { params: { sheetId } }) => ({
  loadSheet: () => dispatch(buildLoadSheet(sheetId))
});

src/client/test/routes/loadSheetIfNeeded.spec.js (test):

const dispatch = sandbox.stub();
      const { mapDispatchToProps } = proxyquire('~/src/sheet/routes/loadSheetIfNeeded', {
        '~/store/SheetLayout': { buildLoadSheet: () => {throw new Error('wtf') } }
      });
      const { loadSheet: mappedLoadSheet } = mapDispatchToProps(dispatch, { params: { sheetId } });
      mappedLoadSheet();

Converted src/client/test/routes/loadSheetIfNeeded.spec.js:

  var dispatch = sandbox.stub();

  var _proxyquire = (0, _proxyquire3.default)('../../src/sheet/routes/loadSheetIfNeeded', {
    '~/store/SheetLayout': { buildLoadSheet: function buildLoadSheet() {
        throw new Error('wtf');
      } }
  }),
      mapDispatchToProps = _proxyquire.mapDispatchToProps;

  var _mapDispatchToProps = mapDispatchToProps(dispatch, { params: { sheetId: sheetId } }),
      mappedLoadSheet = _mapDispatchToProps.loadSheet;

  mappedLoadSheet();

so ... I guess the plug-in just sees '~/store/SheetLayout' as a plain old string and not a path, and thus doesn't convert it?

Oh gotcha, the path also exists in the 2nd argument.. Yeah.. The transformFunctions mechanism only change the path in the first argument. We cannot really do anything about the rest :/

proxyquire looks like a very special case.

You could build a wrapper to get the path? Like..

const getTransformedPath = (path) => path;

// then you use it like that:

const { doBar } = proxyquire('~/src/some/path/bar', {
   [getTransformedPath('~/src/some/path/foo')]: { doFoo: () => { throw new Error('proxied') }}
});

Even if in the code getTransformedPath returns the same thing. In the end, it will return the real path if you add getTransformedPath as a transformFunction for the plugin.

You see what I mean?

That makes perfect sense, and I'll use this pattern to solve my issue. Thanks!

It might even make sense to make this an exportable function. Obviously the getTransformedPath function (or applyPathAliases? aliasPath? it seems like a shorter name would be preferable since it's not a very significant function ... but I digress) is just a no-op/passthrough function. However, I do see a couple of benefits of standardizing it:

const { aliasPath } = require('babel-plugin-module-resolver');
// ...
   [aliasPath('~/src/some/path/foo')]: someMock

or (using template tags):

   [aliasPath`~/src/some/path/foo`]: someMock

A) it would make users' code more consistent (everyone would use the same no-op function instead of each rolling their own version)

B) it would give you a function to hang documentation on (so the docs can explain how to transform arbitrary path strings)

I realize that this use case is mostly limited to proxyquire, but because it's a fairly popular library and because users might have other causes to transform paths on the fly (eg. to error log a post-transformed path) I could potentially see some value to an official getTransformedPath.

Just a thought, but regardless thanks for all the assistance on this ticket.

Yep, might be a good idea, indeed. It doesn't hurt to have a passthrough function to have the real path. Do you want to open a PR? I'm bad finding good function name, but I'd probably go with resolve (to be in line with require.resolve in nodejs)

Open to create a PR? :)

Any opinion @fatfisz ?

I'm bad finding good function name, but I'd probably go with resolve (to be in line with require.resolve in nodejs)

Short and fits well, sounds great to me.

Open to create a PR? :)

Sure. Can't promise it will be today, but that should be fine since I'll want to wait to hear back from @fatfisz first anyway.

P.S. My planned changes would be to:

  1. Add resolve to the default list of transformFunctions
  2. Define an export const resolve = x => x; in the appropriate file to make it a package export (have to research which file this)
  3. Add documentation to the appropriate place (again I'll have to research where) explaining what resolve is and how to use it.

This is a great solution, I say go for it ;)

Btw. the file is src/index.js.

Hmm.. Yeah.. After reflection, maybe it's not a good idea to have a super generic name in the transformFunctions option :/

resolveAlias then? Because we're dealing with aliases after all.

It's actually for everything (root and alias). So using alias in the name might be confusing.

I'm starting to wonder if it's really a good idea. I checked a couple name on github to see the number of occurrences and there are a lot of mentions for resolveAlias or resolvePath.

Also, you don't really want to import the plugin right inside your own project code, so... :/ Maybe it's best to let the end user define a function after all? And if we see later that there's a demand for something like this, we can revisit our options?

Maybe it's best to let the end user define a function after all? And if we see later that there's a demand for something like this, we can revisit our options?

That's totally reasonable, but then would it maybe make sense to have a quick note about proxyquire/passthrough functions/transformFunctions somewhere in the docs? Even just a sentence or two (similar to the ESLint note) might help other proxyquire users.

@machineghost Would you be willing to prepare a PR with such a note? I think that right now somewhere around "Usage with React Native" and "Usage with Flow" in DOCS.md would be the right place to put it.

Was this page helpful?
0 / 5 - 0 ratings