Skip to content

Compat Path

GitHub Actions edited this page Sep 18, 2026 · 1 revision

Compat-Path

Cross-runtime path manipulation utilities with a unified API.

Deno Bun Node.js

Table of Contents

Overview

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.

Features

Feature Bun Deno Node.js
Path joining ✅ ✅ ✅
Path resolution ✅ ✅ ✅
Path parsing ✅ ✅ ✅
Path formatting ✅ ✅ ✅
Platform support ✅ ✅ ✅

Installation

Deno:

deno add @tundralibs/compat

Bun:

bunx jsr add @tundralibs/compat

Node.js:

npx jsr add @tundralibs/compat

Constants

Path Separators

const 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 OS

Example:

import { DELIMITER, SEPARATOR } from '@tundralibs/compat/path';

console.log(`Separator: ${SEPARATOR}`); // / on Unix, \ on Windows
console.log(`Delimiter: ${DELIMITER}`); // : on Unix, ; on Windows

SEPARATOR_PATTERN is 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') is false on Unix and true on Windows — verified directly, not inferred.

API Reference

join()

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

dirname()

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'

basename()

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'

extname()

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

resolve()

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'

normalize()

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'

isAbsolute()

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')); // false

relative()

Computes 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'

parse()

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'
// }

format()

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

base always wins over name + ext when both are present — verified against the underlying node:path/@std/path format(). Pass one or the other, not both, unless you're relying on that precedence deliberately.

Examples

Building File Paths

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';
}

Path Transformations

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'

Relative Path Navigation

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'

Platform-Aware Path Handling

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, '\\');
}

Path Parsing and Validation

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'
// }

Platform Considerations

Windows vs Unix

The module automatically handles platform differences:

  • Separators: Uses / on Unix, \ on Windows
  • Roots: Unix paths start with /, Windows with drive letter C:\
  • Case sensitivity: Windows is case-insensitive, Unix is case-sensitive

Cross-Platform Best Practices

  1. Always use path functions - Never manually concatenate paths
  2. Use forward slashes in code - Convert at runtime with normalize()
  3. Test on target platforms - Path behavior can differ subtly
  4. 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

← Back to Compat

Clone this wiki locally