Wednesday, 7 June 2017

Nodejs: require nested modules from outside of node_modules

The nodejs module loading algorithm is well documented, however I am not able to find a reliable method of requiring modules which reside outside of the node_modules directory.

I'm manipulating the node_modules to accommodate modules which do not come from the npm registry but are still managed by npm (and in time any other manager which supports installing from a tarball).

My modules directory is set up as:

/var/project
├── A@1.0.0
└─┬ B@1.0.0
  ├── A@2.0.0

where the A@x.x modules are from the npm registry (3rd party) and the B@1.x module is local (actually part of a composer PHP package). The B module is dynamically created as a .tar.gz from the package.json within the composer package. Installing as a tar allows npm to also install dependencies and work its magic with conflicting versions.

The generated package.json has a main field pointing to a file (let's call it vendor.js) within /var/project/vendor. My initial idea was that since the projects node_modules directory is common to all, any require() calls from vendor would be resolved. However this is not the case when node_modules is not flat.

A call to require('A') from vendor.js will resolve to A@1.0.0, but it's actual dependency is A@2.x. Common solutions to this problem are

  1. npm install <dir>. This copies ALL files from <dir> to the node_modules directory.
  2. npm link. Doesn't work correctly on node prior to 6.x, also creates files within the /var/vendor directory which is read only (or should be since it belongs to composer).
  3. cd /var/project/vendor/B && ln -s /var/project/node_modules/B/node_modules. Again creates symlinks within vendor which may become danglers when npm dedupe is run.
  4. Like (3) but symlink in the opposite direction. require() resolve symlinks to their real path pre 6.x and requires a --preserve-symlinks CLI arg on later versions.

After digging through the nodejs sources, I think I have found a solution which doesn't involve symlinks and/or writing to vendors. Rather than having the dynamic package.json main field point to an external source. I include a thunk, basically a wrapper around require which begins the search from the relative path:

var Module = require('module');
var oldnodeModulePaths = Module._nodeModulePaths;

// Pretend the module is local
Module._nodeModulePaths = function(from) {
    return oldnodeModulePaths.call(this, __dirname);
};

require('/var/project/vendor/B');

Module._nodeModulePaths = oldnodeModulePaths;

This appears to work with all require calls first looking within the nested node_modules directory. My problem with this solution is that it depends on an undocumented feature.

Is there a better, more legal, way of accomplishing what I want, or does an npm module already exist (relieving myself the burden of maintaining the above snippet). Also, how will compilers like webpack handle with this?

Essentially, all of the above is to enable composer packages to supply node aware scripts, pass through gulp/grunt/webpack..., with the end goal of being rendered in the browser. Ultimately freeing the developers time for a coffee.



via Twifty

No comments:

Post a Comment