path
js/file/path.ts
fino:file/path — POSIX path manipulation.
Provides an immutable Path class and a set of module-level functions for
working with filesystem path strings. All operations are purely in-memory
string transformations — no filesystem access, no stat() calls. For actual
filesystem I/O, see fino:file.
The separator and absolute-path rules are chosen once at startup from the
host platform (via internal:process). Fino runs on POSIX systems (macOS,
Linux), where the separator is / and backslashes are ordinary filename
characters — the Windows branches in this module are inert on POSIX hosts.
Immutability
Path instances are immutable: normalize(), join(), resolve(), and
relative() all return new Path instances. The internal #path string
is never modified after construction. This makes Path objects safe to pass
around without defensive copies.
Path.from(input) is a coercion helper: if the input is already a Path,
it returns it directly (no allocation). If it's a string, it wraps it in a
new Path. Use Path.from() in hot paths to avoid unnecessary wrapping.
Path normalization
normalize() collapses repeated separators and resolves . and ..
segments. Key behavior:
- Empty string →
'.'(current directory, like POSIX realpath) - Leading slash is preserved (path stays absolute)
- Trailing slash is preserved (caller's intent to refer to a directory)
..at the root of an absolute path is silently ignored (can't go above root)..at the start of a relative path is preserved (we can't resolve it without knowing the current directory)
Path resolution vs. joining
join(...segments) concatenates segments with the separator and normalizes
the result. It does not produce absolute paths from relative ones.
resolve(...segments) processes segments from right to left, stopping at
the first absolute segment. This mirrors Node.js path.resolve and POSIX
shell path building: resolve('/a', 'b', '/c', 'd') → /c/d (starts over
at /c).
relative(from, to) computes a relative path from from to to by
finding the longest common prefix of normalized segments, then emitting
.. for each diverging segment in from followed by the remaining
segments of to.
Module-level functions vs. Path methods
Both exist for ergonomic reasons. The class methods operate on an existing
Path instance. The module-level functions accept string|Path and
delegate to the class, covering the common case where callers have raw
strings and don't want to construct a Path just to call one method.
import { Path, join, resolve, relative, dirname, basename } from 'fino:file/path';
const p = new Path('/usr/local/bin');
p.dirname() // Path('/usr/local')
p.basename() // 'bin'
p.join('node') // Path('/usr/local/bin/node')
const rel = new Path('./src/../lib/index.mjs');
rel.normalize() // Path('lib/index.mjs')
rel.extname() // '.mjs'
resolve('/home', 'user', 'docs') // Path('/home/user/docs')
relative('/a/b', '/a/b/c/d') // Path('c/d')
Classes
class Path {
An immutable representation of a filesystem path. All mutation methods return new Path instances.
Constructor accepts a string or another Path. Use Path.from() for
coercion that passes Path instances through without allocating.
Paths are normalized only when a method such as normalize(), join(), or
resolve() asks for normalization. Constructing a Path preserves the raw
input string, including relative segments and repeated separators.
import { Path } from 'fino:file/path';
const source = new Path('./src/../src/main.ts');
const normalized = source.normalize();
console.log(source.toString()); // ./src/../src/main.ts
console.log(normalized.toString()); // src/main.ts
Constructors
constructor(input: string | Path)
Create a path wrapper from a raw path string or another Path.
The constructor does not touch the filesystem and does not normalize the
input. Passing another Path copies its stored string. Use Path.from()
when you want to avoid allocating a new wrapper for existing Path
instances.
import { Path } from 'fino:file/path';
const raw = new Path('/tmp//cache');
const copy = new Path(raw);
console.log(copy.toString()); // /tmp//cache
Static Methods
static from(input: string | Path): Path
Convert a string or Path to a Path. If the input is already a Path, returns it directly (no copy).
This is useful for APIs that accept string | Path and want a stable
object form. The returned object preserves the raw input string and may be
the same object that was passed in.
import { Path } from 'fino:file/path';
const existing = new Path('/var/log');
console.log(Path.from(existing) === existing); // true
console.log(Path.from('tmp').join('out').toString()); // tmp/out
Methods
dirname(): Path
Return the directory portion of the path (everything before the last
separator). Equivalent to POSIX dirname.
The result is a new Path. Relative paths with no separator return ..
Trailing separators are ignored except when the path is the root
separator.
import { Path } from 'fino:file/path';
console.log(new Path('/usr/local/bin/').dirname().toString()); // /usr/local
console.log(new Path('README.md').dirname().toString()); // .
basename(suffix?: string): string
Return the final component of the path. If suffix is provided and
matches the end of the basename, it is removed.
The method performs string manipulation only. It strips trailing
separators before finding the final component, and it removes suffix
only when the full basename ends with that exact string. The root path
/ returns an empty string.
import { Path } from 'fino:file/path';
console.log(new Path('/tmp/archive.tar.gz').basename('.gz')); // archive.tar
console.log(new Path('/tmp/build/').basename()); // build
extname(): string
Return the file extension (including the leading dot), or an empty string if there is none.
Leading-dot names such as .env are treated as having no extension.
Compound extensions are not special-cased; only the substring after the
final dot is returned.
import { Path } from 'fino:file/path';
console.log(new Path('server.test.ts').extname()); // .ts
console.log(new Path('.env').extname()); // ''
isAbsolute(): boolean
True if the path is absolute.
On POSIX, an absolute path starts with /. The check does not verify that
the path exists.
import { Path } from 'fino:file/path';
console.log(new Path('/tmp').isAbsolute()); // true
console.log(new Path('./tmp').isAbsolute()); // false
normalize(): Path
Return a normalized version of this path: collapse multiple separators,
resolve . and .. segments.
Normalization is lexical. It does not resolve symlinks, inspect the filesystem, or make relative paths absolute. A trailing separator is preserved when present.
import { Path } from 'fino:file/path';
const path = new Path('/tmp//cache/../logs/');
console.log(path.normalize().toString()); // /tmp/logs/
join(...segments: (string | Path)[]): Path
Join this path with one or more additional segments.
Segments are concatenated with the platform separator and normalized. Empty
segments are ignored. Absolute later segments are not treated as a reset;
use resolve() for right-to-left absolute path resolution.
import { Path } from 'fino:file/path';
const output = new Path('/tmp').join('build', '..', 'dist/app.js');
console.log(output.toString()); // /tmp/dist/app.js
resolve(...bases: (string | Path)[]): Path
Resolve this path against one or more base paths, producing an absolute path. Processes from right to left; the first absolute path wins.
Bases are listed outermost first: the leftmost base is consulted only when nothing to its right is absolute. If no segment is absolute at all, the result stays relative.
import { Path } from 'fino:file/path';
const path = new Path('app.js').resolve('/srv/www', 'assets');
console.log(path.toString()); // /srv/www/assets/app.js
relative(from: string | Path): Path
Return a relative path from from to this path.
Both paths are normalized before computing the relation.
The calculation is lexical and does not verify either path. When both
normalized paths are the same, the result is ..
import { Path } from 'fino:file/path';
const target = new Path('/repo/src/app.ts');
console.log(target.relative('/repo/tests').toString()); // ../src/app.ts
toString(): string
Return the raw stored path string.
This does not normalize or resolve the path, so it may include repeated
separators, . segments, or .. segments exactly as supplied.
import { Path } from 'fino:file/path';
console.log(new Path('./a/../b').toString()); // ./a/../b
toJSON(): string
Serialize the path as its raw string for JSON.stringify().
The returned value matches toString() and is not normalized.
import { Path } from 'fino:file/path';
console.log(JSON.stringify({ file: new Path('src/main.ts') }));
Functions
function join(...segments: (string | Path)[]): Path
Join path segments.
This is the module-level form of Path#join(). It ignores empty segments,
joins the remaining segments with the platform separator, and normalizes the
result. With no usable segments, it returns Path('.').
import { join } from 'fino:file/path';
console.log(join('src', '..', 'dist', 'app.js').toString()); // dist/app.js
function resolve(...segments: (string | Path)[]): Path
Resolve a sequence of paths into an absolute path.
Segments are processed from right to left until an absolute segment is found, then the result is normalized. If no absolute segment is present, the returned path remains relative.
import { resolve } from 'fino:file/path';
console.log(resolve('/srv', 'app', '/tmp', 'file.txt').toString()); // /tmp/file.txt
function normalize(p: string | Path): Path
Normalize a path string.
This is a lexical operation. It collapses repeated separators and resolves
. and .. segments without inspecting the filesystem or resolving
symlinks.
import { normalize } from 'fino:file/path';
console.log(normalize('/tmp//a/../b').toString()); // /tmp/b
function dirname(p: string | Path): Path
Return the directory name of a path.
The input is coerced with Path.from() and handled like Path#dirname().
Relative paths with no separator return Path('.').
import { dirname } from 'fino:file/path';
console.log(dirname('/var/log/system.log').toString()); // /var/log
function basename(p: string | Path, suffix?: string): string
Return the basename of a path, optionally stripping a suffix.
Trailing separators are ignored. The suffix is removed only when it exactly matches the end of the final path component.
import { basename } from 'fino:file/path';
console.log(basename('/tmp/report.csv', '.csv')); // report
function extname(p: string | Path): string
Return the extension of a path.
The extension includes the leading dot. Names without a dot, and leading-dot
names such as .env, return an empty string.
import { extname } from 'fino:file/path';
console.log(extname('server.test.ts')); // .ts
function isAbsolute(p: string | Path): boolean
Check if a path is absolute.
This is a string predicate only and does not check whether the path exists.
import { isAbsolute } from 'fino:file/path';
console.log(isAbsolute('/tmp')); // true
function relative(from: string | Path, to: string | Path): Path
Compute a relative path from from to to.
Both paths are normalized before comparison. The calculation is lexical and
does not access the filesystem. When both paths normalize to the same
location, the result is Path('.').
import { relative } from 'fino:file/path';
console.log(relative('/repo/docs', '/repo/src/app.ts').toString()); // ../src/app.ts
Constants
const sep
Platform path separator used by this module.
Fino currently runs on POSIX hosts, so this is usually /. Code that formats
user-visible paths can import this constant instead of hard-coding a
separator.
import { sep } from 'fino:file/path';
console.log(['tmp', 'cache'].join(sep));