Skip to main content

priv/ts/node-path.ts

const sep = '/'
const delimiter = ':'

function isAbsolute(p: string): boolean {
  return p.startsWith('/')
}

function normalize(p: string): string {
  if (p === '') return '.'
  const absolute = isAbsolute(p)
  const parts = p.split('/')
  const stack: string[] = []
  for (const part of parts) {
    if (part === '' || part === '.') continue
    if (part === '..') {
      if (stack.length > 0 && stack[stack.length - 1] !== '..') stack.pop()
      else if (!absolute) stack.push('..')
    } else {
      stack.push(part)
    }
  }
  let result = stack.join('/')
  if (absolute) result = '/' + result
  return result || (absolute ? '/' : '.')
}

function join(...parts: string[]): string {
  return normalize(parts.filter(Boolean).join('/'))
}

function resolve(...parts: string[]): string {
  let resolved = ''
  for (let i = parts.length - 1; i >= 0; i--) {
    const p = parts[i]
    if (!p) continue
    resolved = resolved ? p + '/' + resolved : p
    if (isAbsolute(p)) break
  }
  if (!isAbsolute(resolved)) {
    const cwd = (globalThis as Record<string, unknown>).process
      ? ((globalThis as Record<string, unknown>).process as { cwd(): string }).cwd()
      : '/'
    resolved = cwd + '/' + resolved
  }
  return normalize(resolved)
}

function basename(p: string, ext?: string): string {
  const parts = p.replace(/\/+$/, '').split('/')
  let base = parts[parts.length - 1] || ''
  if (ext && base.endsWith(ext)) base = base.slice(0, -ext.length)
  return base
}

function dirname(p: string): string {
  const parts = p.replace(/\/+$/, '').split('/')
  parts.pop()
  const result = parts.join('/')
  return result || (isAbsolute(p) ? '/' : '.')
}

function extname(p: string): string {
  const base = basename(p)
  const dot = base.lastIndexOf('.')
  if (dot <= 0) return ''
  return base.slice(dot)
}

interface ParsedPath {
  root: string
  dir: string
  base: string
  ext: string
  name: string
}

function parse(p: string): ParsedPath {
  const dir = dirname(p)
  const base = basename(p)
  const ext = extname(p)
  const name = ext ? base.slice(0, -ext.length) : base
  return { root: isAbsolute(p) ? '/' : '', dir, base, ext, name }
}

function format(obj: Partial<ParsedPath>): string {
  const dir = obj.dir || obj.root || ''
  const base = obj.base || (obj.name || '') + (obj.ext || '')
  return dir ? (dir === '/' ? dir + base : dir + '/' + base) : base
}

function relative(from: string, to: string): string {
  const fromParts = resolve(from).split('/').filter(Boolean)
  const toParts = resolve(to).split('/').filter(Boolean)
  let i = 0
  while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) i++
  const ups = Array(fromParts.length - i).fill('..')
  return [...ups, ...toParts.slice(i)].join('/') || '.'
}

const path = {
  sep,
  delimiter,
  posix: null as unknown,
  win32: null as unknown,
  isAbsolute,
  normalize,
  join,
  resolve,
  basename,
  dirname,
  extname,
  parse,
  format,
  relative,
}

// Self-reference for Node.js compat
path.posix = path
path.win32 = path

;(globalThis as Record<string, unknown>).path = path