Non-Module File Paths Are Relative To The "Working Directory" In Node.js
In my post yesterday about calculating module-relative file paths with require.resolve() in Node.js, I drew a distinction between "module paths" and "file paths". In Node, modules are loaded using a well documented algorithm that accounts for both relative and non-relative paths. Outside of the module loader, however, "file paths" are either absolute to the file system or relative to the "working directory" of the current process. By default, the working directory is the directory from which the node process was launched; but, this can be changed programmatically from within the node execution.
If you look in the path module documentation for Node, it outlines the fact that "." will resolve to the current working directory:
path.resolve( "." )
From that, we can gather that any non-absolute file path will also be relative to the working directory:
path.resolve( "some/relative/path" )
Based on this, we can then assume that any file operation that accepts a file path - such as fs.readFileSync(path) - will operate on the given path based on the path resolution policy. This means that fs (file system) operations should see files as either "absolute" or, relative to the working directory.
To illustrate this relationship to the working directory, I've created a simple demo in which the main app module is in a sub-directory. I then created two different "data.txt" files - one in the root directory and one in the sub-directory - which the app module will attempt to consume:
- ./demo/data.txt
- ./demo/sub/app.js
- ./demo/sub/data.txt
The app module will consume these data files using a relative file path:
fs.readFileSync( "data.txt" )
Based on where we launch the node process, this working-directory-relative path should load a different data file. And, to make things even more interesting, I will be using the process.chdir() method to programmatically change the working directory mid-execution:
// Require the core node modules.
var chalk = require( "chalk" );
var fileSystem = require( "fs" );
var path = require( "path" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// When we are dealing with a "file path" on the file-system (ie, NOT A MODULE PATH)
// the "." resolves to the current "working directory". This means that any file-path
// that starts with "./" is going to be relative to the working directory. By default,
// the working directory is going to be the directory from which the node process
// was launched.
console.log( chalk.red.bold( "Working Directory:" ), process.cwd() );
console.log( chalk.red.bold( "Resolved Directory:" ), path.resolve( "." ) );
console.log(
chalk.red( "File Content:" ),
// Here, we are using a relative file-path to load "data.txt". This means that
// this file will be loaded relative to the "working directory".
chalk.italic.grey( fileSystem.readFileSync( "data.txt" ).toString() )
);
// Within the node process, we have the option to change the directory used for the
// current working directory. For example, we can change it to the __dirname, which
// will make this MODULE DIRECTORY the current working directory, regardless of where
// the node process was launched.
console.log( "" );
console.log( chalk.dim.italic( "Changing wd to", __dirname ) );
console.log( "" );
// Change "working directory" to be the current MODULE directory.
process.chdir( __dirname );
// Now that we've changed the "working directory", let's re-run the previous test
// with the relative-path "data.txt" file read.
console.log( chalk.red.bold( "Working Directory:" ), process.cwd() );
console.log( chalk.red.bold( "Resolved Directory:" ), path.resolve( "." ) );
console.log(
chalk.red( "File Content:" ),
chalk.italic.grey( fileSystem.readFileSync( "data.txt" ).toString() )
);
Now, let's run this node.js app from the root directory of the demo, invoking app.js from a sub-directory file path:
First, we can see that the working directory is the same directory that path.resolve(".") resolves to. This is why the first file-read of "data.txt" reads the file from the root of the demo, one directory up from app.js (ie, where the node process was launched). However, when we programmatically switch the working directory to be the current module directory:
process.chdir( __dirname );
... re-running the logging and the file-read shows us that the second file-read of "data.txt" reads from the sub-directory despite no pathing changes in the file read operation.
As I said before, the working directory defaults to where the node process was launched; so, if we go down into the sub-directory and launch the node process there, we should change the working directory:
As you can see, this time, since we are launching the node process from the sub-directory, the sub-directory is the working directory. This means that both reads of "data.txt" will be relative to the sub-directory (ie, the working directory).
This concept of the working directory creates an intersting coupling in a node application and a node ecosystem. For a module to rely on the location of the working directory, it may have to assume a few things:
- The node process was launched from a known location.
- The main app module programmatically changed the working directory to a known location.
- No other modules changed the working directory location.
These assumption aren't necessarily a bad thing. And, I can certainly see a use-case in which the app module always changes the working directory to be the current module directory. This way, the file path operations within the app can always be "app relative", regardless of how we are launching node. We often rely on application-wide mappings to help resolve file-paths - so, I don't really see this as being any different.
And, of course, if you don't want to rely on the location of the working directory, you can always construct absolute file paths using your module's __dirname.
I know for me, the use of file paths - particularly relative file paths - is always a bit magical until you understand the rules that govern the consumption. For me, seeing how the location of the node process affects the file reads really made things a lot more concrete. And, knowing that I can programmatically change the working directory gives me some ideas about how I want to approach my current Express.js studies.
Want to use code from this post? Check out the license.
Reader Comments
You sir, are my nodeJS hero! Thanks for writing this out so clearly and concisely!
@Yhc,
My pleasure, good sir -- I'm glad you found this interesting.
Yes, totally saved me today-
@Chris,
Awesome! Glad to be able to help :D
Thank you soooo much. Was refactoring and got super puzzled when my require paths worked as expected but not my FilePaths.
Having someone else explore this and write it all out nicely is very handy.
@Nate,
My pleasure, good sir! That's why the JavaScript community is so awesome. Teamwork all the way down :D