import type { Stats } from 'node:fs' import { fs } from 'memfs' import type { JsonValue } from 'type-fest' import { L, OT, pipe, T } from './effect/index.js' import type { SymlinkType } from './fs_.js' import { FileNotFoundError, FileOrDirNotFoundError, FsTag, JsonParseError, JsonStringifyError, MkdirError, ReadFileError, RmError, StatError, SymlinkError, WriteFileError, } from './fs_.js' export const fileOrDirExists = (pathLike: string): T.Effect => { return pipe( stat(pathLike), T.map((stat_) => stat_.isFile() || stat_.isDirectory()), T.catchTag('fs.FileNotFoundError', () => T.succeed(false)), T.tap((exists) => OT.addAttribute('exists', exists)), OT.withSpan('fileOrDirExists', { attributes: { pathLike } }), ) } export const symlinkExists = (pathLike: string): T.Effect => { return pipe( stat(pathLike), T.map((stat_) => stat_.isSymbolicLink()), T.catchTag('fs.FileNotFoundError', () => T.succeed(false)), ) } export const stat = (filePath: string): T.Effect => { return T.tryCatch( () => fs.statSync(filePath), (error: any) => { if (error.code === 'ENOENT') { return new FileNotFoundError({ filePath }) } else { return new StatError({ filePath, error }) } }, ) } export const readFile = (filePath: string): T.Effect => OT.withSpan('readFile', { attributes: { filePath } })( T.tryCatch( () => fs.readFileSync(filePath, 'utf8') as string, (error: any) => { if (error.code === 'ENOENT') { return new FileNotFoundError({ filePath }) } else { return new ReadFileError({ filePath, error }) } }, ), ) export const readFileBuffer = (filePath: string): T.Effect => OT.withSpan('readFileBuffer', { attributes: { filePath } })( T.tryCatch( () => fs.readFileSync(filePath, { encoding: 'buffer' }) as Buffer, (error: any) => { if (error.code === 'ENOENT') { return new FileNotFoundError({ filePath }) } else { return new ReadFileError({ filePath, error }) } }, ), ) export const readFileJson = ( filePath: string, ): T.Effect => pipe( readFile(filePath), T.chain((str) => T.tryCatch( () => JSON.parse(str) as T, (error) => new JsonParseError({ str, error }), ), ), ) export const readFileJsonIfExists = ( filePath: string, ): T.Effect => pipe( fileOrDirExists(filePath), T.chain((exists) => (exists ? readFileJson(filePath) : T.succeed(undefined))), T.catchTag('fs.FileNotFoundError', (e) => T.die(e)), ) export const writeFile = (filePath: string, content: string): T.Effect => OT.withSpan('writeFile', { attributes: { filePath } })( T.tryCatch( () => fs.writeFileSync(filePath, content, { encoding: 'utf8' }), (error) => new WriteFileError({ filePath, error }), ), ) export const writeFileJson = ({ filePath, content, }: { filePath: string content: JsonValue }): T.Effect => pipe( T.tryCatch( () => JSON.stringify(content, null, 2) + '\n', (error) => new JsonStringifyError({ error }), ), T.chain((contentStr) => writeFile(filePath, contentStr)), ) export const mkdirp = (dirPath: T): T.Effect => OT.withSpan('mkdirp', { attributes: { dirPath } })( T.tryCatch( () => fs.mkdirSync(dirPath, { recursive: true }), (error) => new MkdirError({ dirPath, error }), ), ) export function rm(path: string, params: { force: true; recursive?: boolean }): T.Effect export function rm( path: string, params?: { force?: false; recursive?: boolean }, ): T.Effect export function rm( path: string, params: { force?: boolean; recursive?: boolean } = {}, ): T.Effect { const { force = false, recursive = true } = params return OT.withSpan('rm', { attributes: { path } })( T.tryCatch( () => fs.rmSync(path, { recursive, force }), (error: any) => { if (error.code === 'ENOENT') { return new FileOrDirNotFoundError({ path }) } else { return new RmError({ path, error }) } }, ), ) } /** * NOTE: symlinks are not supported widely on Windows */ export const symlink = ({ targetPath, symlinkPath, type, }: { targetPath: string symlinkPath: string type: SymlinkType }): T.Effect => OT.withSpan('symlink', { attributes: { targetPath, symlinkPath, type } })( T.tryCatch( () => fs.symlinkSync(targetPath, symlinkPath, type), (error) => new SymlinkError({ targetPath, symlinkPath, type, error }), ), ) export const InMemoryFsLive = L.fromValue(FsTag)({ fileOrDirExists, symlinkExists, stat, readFile, readFileBuffer, readFileJson, readFileJsonIfExists, writeFile, writeFileJson, mkdirp, rm, symlink, })