How to correctly calculate path to a Node.js module to access files in that module directory

By: (plus.google.com) +David Herron; Date: May 9, 2019

Tags: Node.JS »»»» JavaScript

Many Node.js modules contain files meant to be accessed as data. For example the Bootstrap, jQuery and Popper.js modules do not contain Node.js code, but exist to distribute the libraries. We are supposed to configure Express (or other framework) to serve the files in those directories to a web browser. The question is -- what is the best way to calculate the filesystem path of a file installed as a Node.js module inside the node_modules hierarchy?

In my book, Node.js Web Development, I made a mistake. I recommended this pattern:

app.use('/assets/vendor/bootstrap/js', express.static( 
  path.join(__dirname, 'node_modules', 'bootstrap', 'dist', 'js'))); 
app.use('/assets/vendor/bootstrap/css', express.static( 
  path.join(__dirname, 'node_modules', 'bootstrap', 'dist', 'css'))); 
app.use('/assets/vendor/jquery', express.static( 
  path.join(__dirname, 'node_modules', 'jquery'))); 
app.use('/assets/vendor/popper.js', express.static( 
  path.join(__dirname, 'node_modules', 'popper.js', 'dist')));  
app.use('/assets/vendor/feather-icons', express.static( 
  path.join(__dirname, 'node_modules', 'feather-icons', 'dist'))); 

Why do this instead of using the CDN? Each of these projects make their file available on a CDN and recommend using that version. What if that 3rd party CDN goes down? That will crash your application, and your users won't care why. By stashing the files on your own server this risk of crashing your application goes away.

To support this, in package.json I told my readers to add dependencies for bootstrap, jquery, popper.js and feather-icons modules. While it is correct to add those dependencies, the exact implementation shown here is incorrect.

The intent is for Express to be configured such that, for example, the /assets/vendor/bootstrap/js route is served from files in node_modules/bootstrap/dist/js, and to serve the other files from their corresponding directories.

The intent of this is good and correct. It is the best way of supplying these files to users of the application. What was not correct is the method to calculate the filesystem path.

The code to configure the static middleware is:

app.use('/path/to/URL', express.static('/path/to/local/directory'));

Whatever is under that directory will be available through the Express server.

There are two problems with path.join(__dirname, 'node_modules', 'module-name', ...)

  1. The __dirname variable is not supported when you are using an ES Module on Node.js, necessitating jumping through some hoops with the import.meta.url variable
  2. More importantly, what happens if the module does not get installed in the topmost node_modules directory? Or what happens if the powers-that-be decide on a different package resolving mechanism?

In other words, I made a fragile assumption that can break in the future. This code recommendation assumed a certain structure for the node_modules directory which could be incorrect.

What would be better is a mechanism to correctly calculate the filesystem path of where the module was installed.

require-it module

The (www.npmjs.com) require-it module provides methods for several capabilities around modules.

Install: npm install require-it --save

And in your code:

// If using CommonJS modules
const { requireIt } = require('require-it');

// Or if using ES Modules
import { requireIt } from 'require-it';

From there two functions are available on the requireIt object ... the resolve method returns the pathname to the main for a named module, and the directory method returns the pathname of the root of the module.

In practice this lets us change the above code to read:

app.use('/assets/vendor/bootstrap/js', express.static( 
  path.join(requireIt.directory('bootstrap'), 'dist', 'js'))); 
app.use('/assets/vendor/bootstrap/css', express.static( 
  path.join(requireIt.directory('bootstrap'), 'dist', 'css'))); 
app.use('/assets/vendor/jquery', express.static( 
  path.join(requireIt.directory('jquery')))); 
app.use('/assets/vendor/popper.js', express.static( 
  path.join(requireIt.directory('popper.js'), 'dist')));  
app.use('/assets/vendor/feather-icons', express.static( 
  path.join(requireIt.directory('feather-icons'), 'dist'))); 

It's a small difference - we have the same result, various local directories are configured to be used by Express - but the application is a little bit less fragile.

In Node.js Web Development, 5th Edition, expect this example to be updated as above ...