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):
node_modules/src
to project-root/src
) - that imports the file as a CommonJS package, not an ES one, which means that named imports don't work"workspaces": ["src"],
in package.json
) - same issue: no named imports"imports": {"#src": "./src"}
) - ignores the --experimental-specifier-resolution=node
flag (so it only works IF I want to go through and manually add .js
to every import in my project)loader.js
file and using node --loader loader.js
) - I couldn't figure out how to make this work, as there is almost no documentation on custom loadersIdeally, 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?
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 ;)