Path

Portable path strings: one set of rules for POSIX, UNC and Windows drive paths, with no file system behind them. Every function here is lexical, so it answers from the text alone.

The model

A path is a root followed by segments. The root is /, // for a UNC path, C:/ for a drive, or nothing for a relative path. It is not a segment, and .. cannot climb above it. Both / and \ separate segments. Every function that normalizes (normalize, parse, root, concat) answers with /; the string helpers join, under and relativize only splice text, so a \ already in their input stays.

import { normalize, parse, root, escapes } from './module.f.js'

normalize('/a/./b/../c')  // '/a/c'
normalize('/../x')        // '/x': nothing above the root
normalize('a/../../x')    // '../x': a relative path keeps its climb
escapes('a/../../x')      // true
root('C:/x')              // 'C:/'
root('//srv/x')           // '//'
parse('/a/b')             // ['a', 'b']: segments only, no root

A Windows path reads the same way:

parse('C:\\a\\..\\b')     // ['b'], and its root is 'C:/'

Functions

FunctionUse it for
normalizeone canonical spelling of a path, root kept
parsethe normalized segments, root dropped
rootwhether, and how, a path is absolute
escapeswhether a relative path climbs above where it starts
concat(a)(b)b resolved against a; an absolute b replaces a
parentthe directory a path is in; parent('/') is /, parent('x') is .
basenamethe last segment, or '' for a root
ancestorsevery directory above a path, outermost first
join(...segments)segments joined with /, with no normalization
under(dir, name)a name below a directory that came from outside
relativize(base, path)path as ./... when it starts with base
isProperPrefixwhether one segment list is a strict ancestor of another
toPosix\ replaced by /, nothing else
concat('a/b')('../c')   // 'a/c'
concat('a/b')('/c')     // '/c'
ancestors('/a/b/c')     // ['/a', '/a/b']
basename('a/b/x.rs/')   // 'x.rs'
join('a', '..', 'b')    // 'a/../b': join does not fold

Use concat when the answer should be normalized and join when the segments are already clean. Use under instead of join for a directory read from a file or the environment: it refuses a bare drive such as C:, which is not a directory.

Import specifiers

import/module.f.mjs reads an import specifier as a URL path and resolves it against the importing file.

import { specifierKind, resolve } from './import/module.f.mjs'

specifierKind('./x')                   // 'relative'
specifierKind('node:fs')               // 'bare': not a path
resolve('src/a.js')('../lib/%20b.js')  // 'lib/ b.js'
resolve('src/a.js')('./x?y')           // null: a URL suffix is refused

Dot segments fold before percent-decoding, and colons, backslashes, ?, # and a leading // are refused, so a specifier can never name a drive, a network path or a parent of the root.

See also

Emergent Testing

Idle. Press Run to start the suite.

  • ./import/proof.f.mjs
  • ./proof.f.mjs