2026-03-30 11:51:07 +00:00
import { chmod , mkdir , readFile , stat as statFile , writeFile } from "fs/promises"
2026-02-19 16:32:32 +00:00
import { createWriteStream , existsSync , statSync } from "fs"
2026-02-18 15:30:52 +00:00
import { lookup } from "mime-types"
2025-12-16 00:01:03 +00:00
import { realpathSync } from "fs"
2026-03-30 03:10:01 +00:00
import { dirname , join , relative , resolve as pathResolve , win32 } from "path"
2026-02-19 16:32:32 +00:00
import { Readable } from "stream"
import { pipeline } from "stream/promises"
2026-02-19 18:40:09 +00:00
import { Glob } from "./glob"
2025-06-01 18:40:44 +00:00
export namespace Filesystem {
2026-02-18 15:30:52 +00:00
// Fast sync version for metadata checks
export async function exists ( p : string ) : Promise < boolean > {
return existsSync ( p )
}
export async function isDir ( p : string ) : Promise < boolean > {
try {
return statSync ( p ) . isDirectory ( )
} catch {
return false
}
}
2026-02-18 21:20:58 +00:00
export function stat ( p : string ) : ReturnType < typeof statSync > | undefined {
return statSync ( p , { throwIfNoEntry : false } ) ? ? undefined
}
2026-03-30 11:51:07 +00:00
export async function statAsync ( p : string ) : Promise < ReturnType < typeof statSync > | undefined > {
return statFile ( p ) . catch ( ( e ) = > {
if ( isEnoent ( e ) ) return undefined
throw e
} )
}
2026-02-18 15:30:52 +00:00
export async function size ( p : string ) : Promise < number > {
2026-02-18 21:20:58 +00:00
const s = stat ( p ) ? . size ? ? 0
return typeof s === "bigint" ? Number ( s ) : s
2026-02-18 15:30:52 +00:00
}
export async function readText ( p : string ) : Promise < string > {
return readFile ( p , "utf-8" )
}
2026-02-18 17:21:25 +00:00
export async function readJson < T = any > ( p : string ) : Promise < T > {
2026-02-18 15:30:52 +00:00
return JSON . parse ( await readFile ( p , "utf-8" ) )
}
export async function readBytes ( p : string ) : Promise < Buffer > {
return readFile ( p )
}
2026-02-19 16:32:32 +00:00
export async function readArrayBuffer ( p : string ) : Promise < ArrayBuffer > {
const buf = await readFile ( p )
return buf . buffer . slice ( buf . byteOffset , buf . byteOffset + buf . byteLength ) as ArrayBuffer
}
2026-02-18 15:30:52 +00:00
function isEnoent ( e : unknown ) : e is { code : "ENOENT" } {
return typeof e === "object" && e !== null && "code" in e && ( e as { code : string } ) . code === "ENOENT"
}
2026-02-19 16:32:32 +00:00
export async function write ( p : string , content : string | Buffer | Uint8Array , mode? : number ) : Promise < void > {
2026-02-18 15:30:52 +00:00
try {
if ( mode ) {
await writeFile ( p , content , { mode } )
} else {
await writeFile ( p , content )
}
} catch ( e ) {
if ( isEnoent ( e ) ) {
await mkdir ( dirname ( p ) , { recursive : true } )
if ( mode ) {
await writeFile ( p , content , { mode } )
} else {
await writeFile ( p , content )
}
return
}
throw e
}
}
export async function writeJson ( p : string , data : unknown , mode? : number ) : Promise < void > {
return write ( p , JSON . stringify ( data , null , 2 ) , mode )
}
2026-02-19 16:32:32 +00:00
export async function writeStream (
p : string ,
stream : ReadableStream < Uint8Array > | Readable ,
mode? : number ,
) : Promise < void > {
const dir = dirname ( p )
if ( ! existsSync ( dir ) ) {
await mkdir ( dir , { recursive : true } )
}
const nodeStream = stream instanceof ReadableStream ? Readable . fromWeb ( stream as any ) : stream
const writeStream = createWriteStream ( p )
await pipeline ( nodeStream , writeStream )
if ( mode ) {
await chmod ( p , mode )
}
}
2026-02-18 15:30:52 +00:00
export function mimeType ( p : string ) : string {
return lookup ( p ) || "application/octet-stream"
}
2025-12-16 00:01:03 +00:00
/ * *
* On Windows , normalize a path to its canonical casing using the filesystem .
* This is needed because Windows paths are case - insensitive but LSP servers
* may return paths with different casing than what we send them .
* /
export function normalizePath ( p : string ) : string {
if ( process . platform !== "win32" ) return p
2026-03-30 03:10:01 +00:00
const resolved = win32 . normalize ( win32 . resolve ( windowsPath ( p ) ) )
2025-12-16 00:01:03 +00:00
try {
2026-03-30 03:10:01 +00:00
return realpathSync . native ( resolved )
2025-12-16 00:01:03 +00:00
} catch {
2026-03-30 03:10:01 +00:00
return resolved
2025-12-16 00:01:03 +00:00
}
}
2026-02-18 15:30:52 +00:00
2026-03-30 03:10:01 +00:00
export function normalizePathPattern ( p : string ) : string {
if ( process . platform !== "win32" ) return p
if ( p === "*" ) return p
const match = p . match ( /^(.*)[\\/]\*$/ )
if ( ! match ) return normalizePath ( p )
const dir = /^[A-Za-z]:$/ . test ( match [ 1 ] ) ? match [ 1 ] + "\\" : match [ 1 ]
return join ( normalizePath ( dir ) , "*" )
}
2026-03-07 05:42:14 +00:00
// We cannot rely on path.resolve() here because git.exe may come from Git Bash, Cygwin, or MSYS2, so we need to translate these paths at the boundary.
2026-03-11 23:26:54 +00:00
// Also resolves symlinks so that callers using the result as a cache key
// always get the same canonical path for a given physical directory.
2026-03-07 05:42:14 +00:00
export function resolve ( p : string ) : string {
2026-03-11 23:26:54 +00:00
const resolved = pathResolve ( windowsPath ( p ) )
try {
return normalizePath ( realpathSync ( resolved ) )
} catch ( e ) {
if ( isEnoent ( e ) ) return normalizePath ( resolved )
throw e
}
2026-03-07 05:42:14 +00:00
}
2026-02-22 23:49:05 +00:00
export function windowsPath ( p : string ) : string {
if ( process . platform !== "win32" ) return p
return (
p
2026-03-07 05:42:14 +00:00
. replace ( /^\/([a-zA-Z]):(?:[\\/]|$)/ , ( _ , drive ) = > ` ${ drive . toUpperCase ( ) } :/ ` )
2026-02-22 23:49:05 +00:00
// Git Bash for Windows paths are typically /<drive>/...
2026-03-07 05:42:14 +00:00
. replace ( /^\/([a-zA-Z])(?:\/|$)/ , ( _ , drive ) = > ` ${ drive . toUpperCase ( ) } :/ ` )
2026-02-22 23:49:05 +00:00
// Cygwin git paths are typically /cygdrive/<drive>/...
2026-03-07 05:42:14 +00:00
. replace ( /^\/cygdrive\/([a-zA-Z])(?:\/|$)/ , ( _ , drive ) = > ` ${ drive . toUpperCase ( ) } :/ ` )
2026-02-22 23:49:05 +00:00
// WSL paths are typically /mnt/<drive>/...
2026-03-07 05:42:14 +00:00
. replace ( /^\/mnt\/([a-zA-Z])(?:\/|$)/ , ( _ , drive ) = > ` ${ drive . toUpperCase ( ) } :/ ` )
2026-02-22 23:49:05 +00:00
)
}
2025-07-08 22:14:24 +00:00
export function overlaps ( a : string , b : string ) {
const relA = relative ( a , b )
const relB = relative ( b , a )
return ! relA || ! relA . startsWith ( ".." ) || ! relB || ! relB . startsWith ( ".." )
}
export function contains ( parent : string , child : string ) {
2025-07-31 00:57:52 +00:00
return ! relative ( parent , child ) . startsWith ( ".." )
2025-07-08 22:14:24 +00:00
}
2025-06-03 19:57:38 +00:00
export async function findUp ( target : string , start : string , stop? : string ) {
2025-06-09 19:28:06 +00:00
let current = start
const result = [ ]
2025-06-01 18:40:44 +00:00
while ( true ) {
2025-06-09 19:28:06 +00:00
const search = join ( current , target )
2026-01-14 17:53:12 +00:00
if ( await exists ( search ) ) result . push ( search )
2025-06-09 19:28:06 +00:00
if ( stop === current ) break
const parent = dirname ( current )
if ( parent === current ) break
current = parent
2025-06-01 18:40:44 +00:00
}
2025-06-09 19:28:06 +00:00
return result
2025-06-01 18:40:44 +00:00
}
2025-07-03 03:27:04 +00:00
2025-07-10 13:37:28 +00:00
export async function * up ( options : { targets : string [ ] ; start : string ; stop? : string } ) {
const { targets , start , stop } = options
let current = start
while ( true ) {
for ( const target of targets ) {
const search = join ( current , target )
2026-01-14 17:53:12 +00:00
if ( await exists ( search ) ) yield search
2025-07-10 13:37:28 +00:00
}
if ( stop === current ) break
const parent = dirname ( current )
if ( parent === current ) break
current = parent
}
}
2025-07-03 03:27:04 +00:00
export async function globUp ( pattern : string , start : string , stop? : string ) {
let current = start
const result = [ ]
while ( true ) {
try {
2026-02-19 18:40:09 +00:00
const matches = await Glob . scan ( pattern , {
2025-07-03 03:27:04 +00:00
cwd : current ,
2025-07-25 01:20:43 +00:00
absolute : true ,
2026-02-19 18:40:09 +00:00
include : "file" ,
2025-07-03 03:27:04 +00:00
dot : true ,
2026-02-19 18:40:09 +00:00
} )
result . push ( . . . matches )
2025-07-03 03:27:04 +00:00
} catch {
// Skip invalid glob patterns
}
if ( stop === current ) break
const parent = dirname ( current )
if ( parent === current ) break
current = parent
}
return result
}
2025-06-01 18:40:44 +00:00
}