Appearance
File Path Resolver
When to use
Use this when you need a single resolve() function that handles three kinds of paths — absolute, relative, and module — and resolves them from the caller's location rather than process.cwd().
The pattern
The resolve() function handles three kinds of paths — absolute, relative, and module — and resolves them from the caller's location rather than process.cwd().
Resolving different path types
A relative path resolves from the calling module's directory. If this file lives at /app/src/models/account.js, the result is an absolute path under that same directory:
javascript
import { resolve } from "@chriscalo/file";
// If this file is /app/src/models/account.js,
// resolves to /app/src/models/queries/list.sql
const sqlPath = resolve("./queries/list.sql");A bare specifier delegates to Node's module resolution algorithm, finding files inside installed packages:
javascript
const configPath = resolve("my-package/config.json");When building higher-level abstractions like template include() functions, pass the caller path explicitly so resolution stays relative to the parent template rather than the include() wrapper:
javascript
const parentFile = "/app/views/layout.ejs";
const childPath = resolve("./header.ejs", parentFile);
// => /app/views/header.ejsUsing in template systems
The three-way resolution is especially useful for template includes where paths can be relative to the parent template, absolute, or module references:
javascript
function createInclude(parentPath, data) {
return function include(includePath, includeData) {
const templatePath = resolve(
includePath, parentPath
);
return render(templatePath, {
...data,
...includeData,
});
};
}Each nested include() resolves paths relative to its own parent template, creating a natural directory-relative model.
Implementing resolve()
The function inspects the path to determine its type, then resolves accordingly. Absolute paths (starting with /) pass through unchanged, relative paths (starting with ./ or ../) resolve from the caller's directory, and bare specifiers delegate to require.resolve().
javascript
import path from "node:path";
import caller from "caller";
export function resolve(pathString, callerPath) {
if (callerPath === undefined) {
callerPath = caller();
}
const { dir } = path.parse(pathString);
const isAbsolute = path.isAbsolute(pathString);
const isRelative =
!isAbsolute && String(dir).startsWith(".");
const isModule = !isAbsolute && !isRelative;
if (isAbsolute) {
return pathString;
} else if (isRelative) {
const callerDir = path.dirname(callerPath);
return path.resolve(callerDir, pathString);
} else if (isModule) {
return require.resolve(pathString);
} else {
throw new Error(
`Can't resolve path: ${pathString}`
);
}
}The callerPath parameter lets you pass the caller explicitly when building higher-level abstractions (like an include() function for templates). When omitted, the caller package reads it from the stack trace.
Trade-offs
require.resolve()for module paths. This works in CommonJS but needs adaptation for pure ESM projects. In ESM, useimport.meta.resolve()instead.callerpackage dependency. WhencallerPathis not passed, the resolver inspects the stack trace. This is convenient but adds a runtime dependency and can behave unexpectedly if called through wrapper functions that add extra stack frames.- No URL support. This pattern works with file system paths only. For
file://URLs (common in ESM withimport.meta.url), convert withfileURLToPath()first. - Security. The resolver does not sandbox paths. Absolute paths and
../traversals can reach anywhere on the file system. Never use with untrusted input.