My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Saturday, June 01, 2013

The node.js Relative Path Case

Right now, it sucks because ___, as @izs told me to start with, I could not find a simple way to resolve a path from a module that exported a function into another one.
Spoiler: the reason I am trying to resolve paths is to load fresh modules runtime in polpetta. However, this might be a bad practice.
@WebReflection Lest I commit malpractice, I must tell you this is a terrible idea. Now warned, rock on with your bad self. Hack away :)
Still, my point is that it might be handy to be able to resolve paths relatively form the invoker regardless the why, even if you should always ask yourself that ;)
This case is quite easy to misunderstand so I'll skip extra explanations now and put down some code.

relative.js

This example file aim is to log ASAP two different path resolutions: the one from the path, passing through the process.cwd(), and the one from the relative.js file itself.
Object.defineProperties(this, {
  parent: {
    get: function () {
      // used later on
      return module.parent;
    }
  },
  resolve: {
    value: function (path) {
      // it will resolve from this file
      // not from the invoker
      return require.resolve(path);
    }
  }
});

// path module resolves relatively
// from the current process.cwd()
console.log(require('path').resolve('.'));

// require resolves relatively from
// the current file
console.log(require.resolve('./relative.js'));
Running this from node terminal will most likely show something like:
require('./test/relative.js');
/Users/yourname/code
/Users/yourname/code/test/relative.js
Neither logs or resolution are actually OK if we would like to resolve relatively from that path.
If we would like to use that module method, talking about the resolve() one, we cannot trust the current path.
// will throw an error
require('./test/relative.js').resolve('./test/relative.js');

// will pass
require('./test/relative.js').resolve('./relative.js');
If we install that module through npm as global, or even local in some super folder, gosh knows where we should start the relative path resolution accordingly with the module itself, you know what I mean?

Being Relative To The Invoker Path

In order to be able to resolve relatively from the invoker, we need to know at least where is the invoker.
Thankfully, this is easy but be aware of the caching problem:
// relative.js
var
  path = require('path'),
  relativeDir = path.dirname(
    module.parent.filename
  )
;
this.resolve = function (module) {
  return require.resolve(
    path.join(relativeDir, module)
  );
};
At this point we can invoke the method as expected without having erros, from the process folder.
// will log the right path
require('./test/relative.js').resolve('./test/relative.js');
Good, we are able to resolve module names, problem is ... only from the very first one that required relative.js due module caching so that module.parent will be one, and only one, for any other module.

A Hacky Solution

In order to avoid the caching problem within the required module itself, I came up with such trick at the end of the file:
// .. same content described above ...

// remove the module itself from the cache
delete require.cache[__filename];
In this way every single module that will require('./some-path/relative.js') will have a fresh new version of that module so that module.parent, and its filename, will be always the right one: how cool is that?
I am able to resolve relatively from any outer module its path the same way require.resolve(path) would do inside that module which is exactly needed and the goal of require-updated so that any module can use paths as if these were resolved from the file itself in order to require some other file, relative, absolute, or globally installed.
Still I believe there should be a better way to do this ... what do you say?

6 comments:

  1. It's funny as I recently solved the exact opposite problem — of preventing specific file reloads by Node.js test runners and other reloaders to get slow initialization code to run only once.

    In the vein of old #include guards and #pragma once's, named it Require Guard (https://github.com/moll/node-require-guard).

    And just like your require-updated, with irony, it removes itself from the require cache. ^_^

    ReplyDelete
  2. nice :) ... this means there are at least two cases where such technique is needed .... and I believe more.

    @izs said that without use cases it does not make sense to provide the ability to have such relative resolution ... well, let's see if somebody else has more cases.

    One question though: I've used __filename instead of module.id, any reaso you would prefer the latter one?

    ReplyDelete
  3. No particular reason to prefer module.id other than possibly slightly better abstraction, but Node's lib/module.js itself quite freely uses paths, so...

    Regarding having it in core, there's Issue #3285: expose a module.resolve method, or require.resolve(path, startPath).

    ReplyDelete
  4. I think in some case dropped cache can be a proper foot-gun as @izs said bot for these 2 cases, and probably others, is like: if you know what you are doing there's no harm.

    I see this situation the equivalent of "stateless functions" VS "scope dependent closures" where first one could be easily serialized and deserialized without problems about the outer scope since independent.

    In such case, it could be meaningful to exportOnce or something similar in order to flag the current module as "do not ever even bother caching it" which is basically what we both need, no matter what we do after that :)

    This would be a cleaner solution, until that, I am actually happy that we can do this at runtime.

    ReplyDelete
  5. I actually think that not having it built-in (e.g with exportOnce), but having it possible imperatively, is the simpler and thereby cleaner solution.

    Otherwise to solve the other side of what Require Guard does in a similar vein would need an exportTrulyForever to match exportOnce and that'll get quite messy.

    The current common way to get a fresh closed over function sounds elegant enough — to have the caller invoke the exported function and to return a new function from there. Depending on requirers is something you and I only seldom need. :)

    ReplyDelete
  6. another use case: require()ing a whole file tree (http://npm.im/require-tree), currently you need (__dirname + '/path').

    ReplyDelete

Note: Only a member of this blog may post a comment.