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
- How does getting
__dirname
back work? - What is your goal?
- Interoperability between
URL
andpath
strings - Final thoughts
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 ofURL
. 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 afile:///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.
The post Alternatives to <code>__dirname</code> in Node.js with ES modules appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/YSnOlr0
via Read more