So, you’ve listened to all the advice and sat down to migrate your code or learn a new standard, but you have questions. ES modules (also known as ESM) are here, but using them is not quite as simple as migrating all your require()
expressions into import
statements.
ES modules were added to Node in Node 13, about the end of 2019. And Node 12—the last version without ESM—is set for “end of life” in April of 2022, so: presuming your systems are being upgraded, there’ll be fewer and fewer places without native support.
__dirname
__dirname
back work?URL
and path
strings__dirname
Yes! This is the point of the post.
If you’re writing an ES module with the mjs
extension (which forces Node into ESM mode), or with {"type": "module"}
set in your package.json
file, or you’re writing TypeScript and running code some other way… you might encounter this error:
ReferenceError: __dirname is not defined in ES module scope
Similarly, other inbuilt globals that were provided to CommonJS code won’t exist. These are __filename
, exports
, module
, and require
.
To get __dirname
(and __filename
) back, you can add code like this to the top of any file that needs it:
import * as url from 'url'; const __filename = url.fileURLToPath(import.meta.url); const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
Great!
__dirname
back work? Any caveats?I’m glad you’re reading on! The above code works because ESM provides a new, standardized global called import.meta.url
. It’s available in all browsers and Node when running module code, and it will be a string like:
"file:///path/to/the/current/file.js"
"file://C:\Path\To\current\file.js" // windows without WSL "https://example.com/source.js" // if this is browser JS
This brings Node inline with ESM in your browser. As JS developers, we need this new global because our code may run anywhere, locally or remote, and the standard URL format gives us support for that. Of course, you might remember that Node can’t directly import from a web URL, but new tooling like Deno can.
The new __dirname
and __filename
variables created in the code above work just like in CommonJS — if you pass them around, they’ll still have the string name of the original file. They’re not variables that suddenly take on the role of pointing to the directory or filename. (This is a long way of saying you probably don’t want to export
them.)
But note that while the helper above, fileURLToPath
, is a quick solution if you’re just trying to upgrade old code, note that it’s not standardized and won’t work if, e.g., your code is shared with the web.
To be fair, this is not really a new problem: __dirname
and __filename
aren’t shared either, but import.meta.url
is! So, using it directly (read on!) actually lets us be more versatile.
Why is it useful to have __dirname
and __filename
within our scripts?
It’s to be able to interact with the world around our code. These are helpful to import other source files, or to operate in a path that is related to our path.
For example, maybe you’ve got a data file that lives as a peer to your code (“yourprogram.js” needs to import “helperdata.txt”). And this is probably why you want __dirname
over __filename
: it’s more about where your file is rather than the file itself.
But! It’s possible to use the inbuilt object URL
, and many of Node’s inbuilt functions, to achieve a variety of goals without having to simply pretend like we’re building CommonJS code.
Before we start, note a few oddities:
URL
instance to base off. (The order, with the smaller changed part first, can trip people up)import.meta.url
value isn’t an instance of URL
. It’s just a string, but it can be used to construct one, so all the examples below need us to create new objectsThere are a couple of reasons for import.meta.url
being a simple string, one of which is that a URL
is mutable. And we have JS’s legacy on the web to thank—if you change window.location.pathname
, you’re modifying the page’s URL to load a new page.
In that way, window.location
itself remains the same object. And in an ES Module, “changing” the URL makes no sense—the script is loaded from one place and we can’t redirect it once that’s happened.
N.B., window.location
isn’t actually a URL, but it acts basically like one.
We can find the path to a file in the same directory as the file by constructing a new URL:
const anotherFile = new URL('helperdata.txt', import.meta.url); console.info(anotherFile.toString()); // prints "file:///path/to/dirname/helperdata.txt"
Okay, that’s great, but you might point out: I still have a URL
object, not a string, and it still starts with file:///
.
Well, the secret is that Node’s internal functions will actually handle a file://
just fine:
import * as fs from 'fs'; const anotherFile = new URL('helperdata.txt', import.meta.url); const data = fs.readFileSync(anotherFile, 'utf-8');
Great! You’ve now loaded some data, without resorting to the path
helper library.
Just like with reading an adjacent file, we can pass a URL
into the dynamic import()
helper:
const script = 'subfolder/other.mjs'; const anotherScript = new URL(script, import.meta.url); const module = await import(anotherScript);
Again, we have a URL
object, which is happily understood by import
.
The URL object works a bit differently than path
helpers when it comes to finding the current directory or navigating folders. The path.dirname
helper is a good example of this — it roughly means “find me the parent path to the current path.” Read on:
path.dirname('/home/sam/testProject/') // '/home/sam/' path.dirname('/home/sam/testProject') // '/home/sam/' path.dirname('/home/sam/') // '/home'
Importantly, note above that path
doesn’t really care about the trailing /
— it only cares if there’s something after it.
To perform a similar operation on a URL, we add the strings .
or ..
(meaning “go up a directory”), but it has subtly different outcomes than path.dirname
. Take a look:
// if import.meta.url is "/my/src/program.js" const dirUrl = new URL('.', import.meta.url); // "file:///my/src/" const dirOfDirUrl = new URL('.', dirUrl); // "file:///my/src/" - no change const parentDirUrl = new URL('..', import.meta.url); // "file://my/" const parentDirOfDirUrl = new URL('..', dirUrl); // "file://my/" - same as above
What we’ve learned here is that URL
cares about the trailing slash, and adding .
to a directory or a file in that directory will always give you a consistent result. There’s similar behavior if you’re going down into a subfolder:
const u1 = new URL('subfolder/file.txt', import.meta.url); // "file:///my/src/subfolder/file.txt" const u1 = new URL('subfolder/file.txt', dirUrl); // "file:///my/src/subfolder/file.txt"
I think this is much more helpful than what the inbuilt features to Node path.dirname
and so on do — because there’s a strong distinction between file and directory.
Of course, your view might differ — maybe you want to get back to simple strings as fast as possible — and that’s fine, but it’s worth understanding URL
‘s semantics. It’s also something that we have available to us on the web, and these rules all apply to https://
schemes just as much as they do to file://
.
URL
and path
stringsAs much as I want to educate you on how URL
works and all its nuances, we as developers who might be interacting with the file system will always eventually want to get back to pure, simple path
strings — like “/Users/Sam/path/to/your/file.js”. You can’t (easily) use URL
to generate relative paths between files, like with path.relative
, and URLs themselves must be absolute (you can’t work on unrooted paths like “relative/path/to/file.js”).
You might know that URLs have a property called pathname
. On the web, this contains the part after the domain you’re opening. But for file://
paths, this contains the whole path — e.g., file:///path/to/file
would be “/path/to/file”.
But wait! Using this directly is actually dangerous for two reasons, which is why at the top of this post I talk about using Node’s inbuilt helper url.fileURLToPath
. This solves two issues for us:
pathname
— on the web, they’re encoded as %20
, which your computer doesn’t understandpathname
So resist the urge to just use a URL’s pathname
and use the helper that I introduced all the way at the top of the file:
const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); // or const pathToFile = url.fileURLToPath('file:///some/path/to/a/file');
In writing this up, I had a couple of thoughts which didn’t really fit anywhere else:
process.cwd()
, and this is just a regular path—like “/foo/bar” — it’s not now a file:///foo/bar/
just because you’re in module modeurl.filePathToURL
helper — it works in reverse. But, you probably won’t need to do this as oftenThanks for reading! Hit me up on @samthor if you have any questions.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
3 Replies to "Alternatives to __dirname in Node.js with ES modules"
This is great. Hope I can apply some of this soon
¡Es una maravilla!
Me sirvió de mucho su conocimiento.
This fails when developing on Windows. The C: trips it up and requires an absolute path so it cannot be removed. Returns “Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol ‘c:'”
If you have a solution, me and the world would love to know.