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:

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));