javascriptnode.jsesmodules

Are Native Node Absolute (ie. Root-Relative) Paths Possible With ES Module Named Imports?


Normally in Node one can useNODE_PATH=./src" to make it so that instead of:

import { foo } from '../../../bar'

You can just do:

import { foo } from 'src/bar'

However, that only works if you use the esm package (ie. node -r esm): NODE_PATH doesn't work with native ES Modules (ie. adding "type": "module" to package.json) ... so what is the modern replacement?

I've tried all of the following, and none of them seem to work (though I may not be using them correctly, and would welcome any clarification):

Ideally, I'd prefer not to have to implement all of Babel/Webpack/Typescript/etc. on my project, just to replace NODE_PATH=./src, but it seems like adding some such tool is the only way now?


Solution

  • It looks like the only viable option ... if you want root-relative imports AND you don't want to specify the .js extension ... is to use a custom loader.

    For reference, the one I made to achieve this was:

    import path from 'path';
    import fs from 'fs';
    
    export function resolve(originalSpecifier, context, defaultResolver) {
      let specifier = originalSpecifier;
    
      try {
        // All my root-relative imports start with "src/"; if you 
        // have other folders you'll need to account for them here
        if (specifier.startsWith('src')) {
          specifier = specifier.replace(/^src/, path.resolve('.') + '/src');
          // If the import is for a directory, get its index.js file
          const itExists = fs.existsSync(specifier);
          let isDirectory = false;
          try {
            isDirectory = fs.lstatSync(specifier).isDirectory();
          } catch (err) {}
          specifier = itExists && isDirectory ? `${specifier}/index` : specifier;
    
          // Add the ".js" extension if not specified
          specifier += specifier.endsWith('.js') ? '' : '.js';
    
          return {
            format: 'module',
            url: new URL(specifier, context.parentURL).href,
          };
        }
      } catch (err) {
        console.error(err);
      }
      // If we're not handling our special cases, just use the
      // default handler
      return defaultResolver(specifier, context);
    }
    

    Then you can use that loader with the --loader option, eg.

    node --loader loader.js index.js
    

    However, it's worth noting that the loader stuff is still under development, and could change (making the above loader invalid) in the future. Probably sometime in 2030, given how slow the Node org is with their development ;)