JavaScript Basics: Locate Module Resources with File URLs
A relative filesystem path normally depends on the process working directory. An ES module can instead resolve a resource beside its own source using new URL(relative, import.meta.url). File URLs also need proper conversion when filenames contain spaces, percent signs, or fragment characters.
These examples were tested on Linux with Node.js v24.19.0. They use built-in APIs only, require no network access, and include assertions that check the results. See the official Node.js ES module documentation and file URL conversion reference.
Prepare a scratch directory
You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8; .mjs selects ES modules. Each example runs independently.
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.01. Read a sibling resource regardless of the working directory
Save this as read-module-resource.mjs:
import assert from 'node:assert/strict';
import { readFile, writeFile, unlink } from 'node:fs/promises';
const resource = new URL('./module-message.txt', import.meta.url);
const originalDirectory = process.cwd();
await writeFile(resource, 'Hello from beside the module!\n', { flag: 'wx' });
try {
process.chdir('/');
const text = await readFile(resource, 'utf8');
assert.equal(text, 'Hello from beside the module!\n');
console.log(text.trim());
console.log(`Still a file URL: ${resource.protocol === 'file:'}`);
} finally {
process.chdir(originalDirectory);
await unlink(resource);
}Run it:
node read-module-resource.mjsOutput:
Hello from beside the module!
Still a file URL: trueNode’s filesystem methods accept file URL objects directly. The module URL provides a stable base even after the working directory changes. This isolated program changes its own process directory only. Exclusive creation with wx refuses to overwrite an existing fixture, and finally removes the file it created.
2. Convert paths without losing special characters
Save this as convert-special-filenames.mjs:
import assert from 'node:assert/strict';
import { pathToFileURL, fileURLToPath } from 'node:url';
const path = '/tmp/report #1 100%.txt';
const url = pathToFileURL(path);
assert.equal(url.href, 'file:///tmp/report%20%231%20100%25.txt');
assert.equal(url.hash, '');
assert.equal(fileURLToPath(url), path);
console.log(url.href);
console.log(fileURLToPath(url));Run it:
node convert-special-filenames.mjsOutput:
file:///tmp/report%20%231%20100%25.txt
/tmp/report #1 100%.txtThis example converts strings only; it creates no file in /tmp. Use pathToFileURL() to encode a filesystem path and fileURLToPath() to convert back. Hand-building file:// strings can mistake # for a fragment. A URL’s pathname can still contain percent escapes and is not a portable replacement for conversion.
3. Separate a directory URL from a filename
Save this as resolve-module-url.mjs:
import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const directory = new URL('.', import.meta.url);
const file = new URL('./settings.json', directory);
assert.equal(directory.protocol, 'file:');
assert.equal(directory.pathname.endsWith('/'), true);
assert.equal(path.dirname(fileURLToPath(file)),
path.dirname(fileURLToPath(import.meta.url)));
assert.equal(path.basename(fileURLToPath(file)), 'settings.json');
console.log(`Directory URL ends with slash: ${directory.pathname.endsWith('/')}`);
console.log(`Resource filename: ${path.basename(fileURLToPath(file))}`);Run it:
node resolve-module-url.mjsOutput:
Directory URL ends with slash: true
Resource filename: settings.jsonnew URL('.', import.meta.url) resolves the containing directory and preserves a trailing slash. A base URL ending in a filename behaves differently from a directory base. Resolution does not check that settings.json exists. Keep resource references trusted: URL resolution alone does not confine arbitrary input to a directory.
Clean up
In the same terminal, remove only the named example files, return to your original directory, and remove the empty scratch directory:
rm -- read-module-resource.mjs convert-special-filenames.mjs resolve-module-url.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.