Sam Thorogood Sam is a former Googler, now CTO of the Australian energy startup Gridcognition, with an interest in all things web.

Alternatives to __dirname in Node.js with ES modules

5 min read 1653

Alternatives To __Dirname In Node

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.

Help, I’m missing __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!

How does getting __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.

What is your goal?

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:

  • URLs are mutable, and we create a new one by passing (a) a string describing what’s changed and (b) a previous URL instance to base off. (The order, with the smaller changed part first, can trip people up)
  • The 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 objects

There 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.

Goal: Load a file

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.

Goal: Dynamically import code

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.

Goal: Performing path-like operations and gotchas

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://.

Interoperability between URL and path strings

As 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:

  • Spaces in filenames won’t work with pathname — on the web, they’re encoded as %20, which your computer doesn’t understand
  • Windows paths aren’t normalized with pathname

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');

Final thoughts

In writing this up, I had a couple of thoughts which didn’t really fit anywhere else:

  • Node in ES Module mode still provides 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 mode
  • You can convert from a string back to a URL with the url.filePathToURL helper — it works in reverse. But, you probably won’t need to do this as often

Thanks for reading! Hit me up on @samthor if you have any questions.

200’s only Monitor failed and slow network requests in production

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. https://logrocket.com/signup/

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. .
Sam Thorogood Sam is a former Googler, now CTO of the Australian energy startup Gridcognition, with an interest in all things web.

2 Replies to “Alternatives to __dirname in Node.js with ES modules”

Leave a Reply