-
Notifications
You must be signed in to change notification settings - Fork 2
Compat Path
Cross-runtime path manipulation utilities with a unified API.
The Path module provides platform-aware path manipulation utilities that work consistently across Windows, macOS, and Linux on all supported runtimes.
Node and Bun delegate to node:path; every other runtime — Deno, browsers, Cloudflare Workers — uses @std/path, which is pure JavaScript. Path manipulation is therefore string work only and needs no filesystem access, so it stays available in edge and browser bundles.
| Feature | Bun | Deno | Node.js |
|---|---|---|---|
| Path joining | ✅ | ✅ | ✅ |
| Path resolution | ✅ | ✅ | ✅ |
| Path parsing | ✅ | ✅ | ✅ |
| Path formatting | ✅ | ✅ | ✅ |
| Platform support | ✅ | ✅ | ✅ |
Deno:
deno add @tundralibs/compatBun:
bunx jsr add @tundralibs/compatNode.js:
npx jsr add @tundralibs/compatconst DELIMITER: string; // Path delimiter (: on Unix, ; on Windows)
const SEPARATOR: string; // Directory separator (/ on Unix, \ on Windows)
const SEPARATOR_PATTERN: RegExp; // Matches one-or-more separators for the current OSExample:
import { DELIMITER, SEPARATOR } from '@tundralibs/compat/path';
console.log(`Separator: ${SEPARATOR}`); // / on Unix, \ on Windows
console.log(`Delimiter: ${DELIMITER}`); // : on Unix, ; on Windows
SEPARATOR_PATTERNis not a universal "matches both/and\" pattern — verified against source: on Windows it's/[\\/]+/(both), but on every other OS (Deno/Bun/Node on Linux/macOS) it's/\/+/, matching only forward slashes.\is a legal filename character on Unix, so it is deliberately left alone there.SEPARATOR_PATTERN.test('a\\b')isfalseon Unix andtrueon Windows — verified directly, not inferred.
Joins path segments into a single path.
function join(...paths: string[]): string;Parameters:
-
...paths- Path segments to join
Returns: Joined path using platform-specific separators
Example:
import { join } from '@tundralibs/compat/path';
const fullPath = join('src', 'components', 'Button.tsx');
// Unix: 'src/components/Button.tsx'
// Windows: 'src\components\Button.tsx'
const configPath = join(process.cwd(), 'config', 'app.json');Returns the directory name of a path.
function dirname(path: string): string;Parameters:
-
path- File or directory path
Returns: Directory containing the path
Example:
import { dirname } from '@tundralibs/compat/path';
console.log(dirname('/home/user/file.txt')); // '/home/user'
console.log(dirname('C:\\Users\\file.txt')); // 'C:\Users'
console.log(dirname('relative/path/file.txt')); // 'relative/path'Returns the last portion of a path.
function basename(path: string, ext?: string): string;Parameters:
-
path- Path to extract basename from -
ext- Optional extension to remove
Returns: Base name of the path
Example:
import { basename } from '@tundralibs/compat/path';
console.log(basename('/home/user/file.txt')); // 'file.txt'
console.log(basename('/home/user/file.txt', '.txt')); // 'file'
console.log(basename('C:\\Users\\document.pdf')); // 'document.pdf'Returns the extension of a path.
function extname(path: string): string;Parameters:
-
path- Path to extract extension from
Returns: File extension including the dot, or empty string if none
Example:
import { extname } from '@tundralibs/compat/path';
console.log(extname('file.txt')); // '.txt'
console.log(extname('archive.tar.gz')); // '.gz'
console.log(extname('README')); // ''
console.log(extname('.gitignore')); // ''Resolves path segments into an absolute path.
function resolve(...paths: string[]): string;Parameters:
-
...paths- Path segments to resolve
Returns: Absolute path
Example:
import { resolve } from '@tundralibs/compat/path';
// Relative to current directory
const absolute = resolve('src', 'components', 'App.tsx');
// From specific root
const configPath = resolve('/etc', 'app', 'config.json');
// Result: '/etc/app/config.json'
// With relative parts
const dataPath = resolve('/home/user', '../shared', 'data.json');
// Result: '/home/shared/data.json'Normalizes a path, resolving .. and . segments.
function normalize(path: string): string;Parameters:
-
path- Path to normalize
Returns: Normalized path
Example:
import { normalize } from '@tundralibs/compat/path';
console.log(normalize('/home/user/../admin/./file.txt'));
// Result: '/home/admin/file.txt'
console.log(normalize('src//components/./Button.tsx'));
// Result: 'src/components/Button.tsx'Determines if a path is absolute.
function isAbsolute(path: string): boolean;Parameters:
-
path- Path to check
Returns: true if absolute, false otherwise
Example:
import { isAbsolute } from '@tundralibs/compat/path';
console.log(isAbsolute('/home/user')); // true (Unix)
console.log(isAbsolute('C:\\Users')); // true (Windows)
console.log(isAbsolute('relative/path')); // false
console.log(isAbsolute('./src/index.ts')); // falseComputes the relative path from one path to another.
function relative(from: string, to: string): string;Parameters:
-
from- Starting path -
to- Destination path
Returns: Relative path from from to to
Example:
import { relative } from '@tundralibs/compat/path';
const rel = relative('/home/user/app', '/home/user/docs/file.txt');
console.log(rel); // '../docs/file.txt'
const rel2 = relative('/data', '/data/logs/app.log');
console.log(rel2); // 'logs/app.log'Parses a path into its components.
function parse(path: string): ParsedPath;
interface ParsedPath {
root: string; // Root path (e.g., '/' or 'C:\')
dir: string; // Directory path
base: string; // File name with extension
ext: string; // File extension
name: string; // File name without extension
}Example:
import { parse } from '@tundralibs/compat/path';
const parsed = parse('/home/user/file.txt');
console.log(parsed);
// {
// root: '/',
// dir: '/home/user',
// base: 'file.txt',
// ext: '.txt',
// name: 'file'
// }
const winPath = parse('C:\\Users\\file.txt');
// {
// root: 'C:\\',
// dir: 'C:\\Users',
// base: 'file.txt',
// ext: '.txt',
// name: 'file'
// }Formats a parsed path object into a path string.
function format(pathObject: Partial<ParsedPath>): string;Parameters:
-
pathObject- Object with path components
Returns: Formatted path string
Example:
import { format } from '@tundralibs/compat/path';
const path = format({
dir: '/home/user',
name: 'file',
ext: '.txt',
});
console.log(path); // '/home/user/file.txt'
// `base`, when present, is used as-is instead of `name` + `ext`.
const path2 = format({
dir: '/home/user',
base: 'document.pdf',
name: 'ignored',
ext: '.ignored',
});
console.log(path2); // '/home/user/document.pdf' — base wins
basealways wins overname+extwhen both are present — verified against the underlyingnode:path/@std/pathformat(). Pass one or the other, not both, unless you're relying on that precedence deliberately.
import { extname, join, resolve } from '@tundralibs/compat/path';
// Build paths relative to project root
const projectRoot = resolve('.');
const srcPath = join(projectRoot, 'src');
const configPath = join(projectRoot, 'config', 'app.json');
// Filter files by extension
function isTypeScriptFile(path: string): boolean {
const ext = extname(path);
return ext === '.ts' || ext === '.tsx';
}import { basename, dirname, extname, join } from '@tundralibs/compat/path';
function changeExtension(filePath: string, newExt: string): string {
const dir = dirname(filePath);
const name = basename(filePath, extname(filePath));
return join(dir, name + newExt);
}
// Usage
const tsFile = 'src/components/Button.tsx';
const jsFile = changeExtension(tsFile, '.js');
// Result: 'src/components/Button.js'import { dirname, extname, relative } from '@tundralibs/compat/path';
function getRelativeImportPath(
fromFile: string,
toFile: string,
): string {
const fromDir = dirname(fromFile);
let relPath = relative(fromDir, toFile);
// Remove extension for imports
const ext = extname(relPath);
if (ext) {
relPath = relPath.slice(0, -ext.length);
}
// Ensure relative path starts with . or ..
if (!relPath.startsWith('.')) {
relPath = './' + relPath;
}
return relPath;
}
// Usage
const importPath = getRelativeImportPath(
'src/pages/Home.tsx',
'src/components/Button.tsx',
);
// Result: '../components/Button'import {
normalize,
SEPARATOR,
SEPARATOR_PATTERN,
} from '@tundralibs/compat/path';
function ensurePlatformPath(path: string): string {
// Collapses runs of the current OS's separator(s) into one SEPARATOR.
// On Windows this also folds stray `/` into `\`; on Unix it only
// dedupes repeated `/` — a literal `\` in the string is left as-is,
// since it's a legal filename character there, not a separator.
return path.replace(SEPARATOR_PATTERN, SEPARATOR);
}
function toUnixPath(path: string): string {
// Convert any path to Unix-style
return path.replace(/\\/g, '/');
}
function toWindowsPath(path: string): string {
// Convert any path to Windows-style
return path.replace(/\//g, '\\');
}import { extname, isAbsolute, parse } from '@tundralibs/compat/path';
interface FileInfo {
path: string;
name: string;
extension: string;
isAbsolute: boolean;
directory: string;
}
function analyzeFilePath(path: string): FileInfo {
const parsed = parse(path);
return {
path,
name: parsed.name,
extension: parsed.ext,
isAbsolute: isAbsolute(path),
directory: parsed.dir,
};
}
// Usage
const info = analyzeFilePath('/home/user/documents/report.pdf');
console.log(info);
// {
// path: '/home/user/documents/report.pdf',
// name: 'report',
// extension: '.pdf',
// isAbsolute: true,
// directory: '/home/user/documents'
// }The module automatically handles platform differences:
-
Separators: Uses
/on Unix,\on Windows -
Roots: Unix paths start with
/, Windows with drive letterC:\ - Case sensitivity: Windows is case-insensitive, Unix is case-sensitive
- Always use path functions - Never manually concatenate paths
-
Use forward slashes in code - Convert at runtime with
normalize() - Test on target platforms - Path behavior can differ subtly
- Avoid assumptions - Don't assume separator or root format
Example:
import { join, normalize } from '@tundralibs/compat/path';
// ✅ Good - Cross-platform
const path1 = join('src', 'components', 'App.tsx');
// ✅ Good - Normalize mixed separators
const path2 = normalize('src/components\\Button.tsx');
// ❌ Bad - Manual concatenation
const path3 = 'src' + '/' + 'components' + '/' + 'App.tsx';
// ❌ Bad - Platform-specific
const path4 = 'C:\\Users\\file.txt'; // Won't work on Unix