@freeword/meta
Version:
Meta package for Freeword: exports all core types, constants, and utilities from the src/ directory.
194 lines (185 loc) • 8.17 kB
text/typescript
import _ /**/ from 'lodash'
import NodeFSP from 'node:fs/promises'
import PathUtils from 'node:path'
// import * as UF from './UF.ts'
import type * as TY from '../types.ts'
import type * as FT from './FilerTypes.ts'
/**
* Helper function to create consistent bad outcomes
* @param err - The error to wrap
* @param gist - The gist of the error (e.g. 'badPath', 'badInput', 'blankPath')
* @param preambleMsg - A message to prepend to the error message
* @param tmi - Additional error extensions to add to the error
* @param pathinfo - The pathinfo object to add to the error
*/
function badOutcome<GT extends FT.FilerGist = FT.FilerGist>(err: Error, gist: GT, preambleMsg: string, tmi: TY.AnyBag, pathinfo: FT.PathinfoT): FT.BadFilerResult<GT>
function badOutcome<GT extends FT.FilerGist = FT.FilerGist>(err: Error, gist: GT, preambleMsg: string, tmi: TY.AnyBag, pathinfo?: FT.PathinfoT | undefined): FT.BadFilerResult<GT>
function badOutcome<GT extends FT.FilerGist = FT.FilerGist>(err: Error, gist: GT, preambleMsg: string, tmi: TY.AnyBag, pathinfo?: FT.PathinfoT | undefined): FT.BadFilerResult<GT> {
const origmsg = err.message
const extError: TY.ExtError = new Error(`${preambleMsg}: ${origmsg}`) as TY.ExtError
const errTMI: Omit<FT.BadFilerResult<GT>, 'err'> = {
...(pathinfo || {}),
...tmi,
ok: false,
gist,
origmsg,
}
extError.extensions = errTMI
return { ...errTMI, err: extError }
}
/**
* Creates directory recursively
* @param anypath - The pathname or pathinfo of the directory to create -- the `abspath` is used and NOT the `dirpath`
* @returns a GoodFilerMkdirResult or a BadFilerMkdirResult
*/
export async function mkdirp(anypath: FT.Anypath): Promise<FT.FilerMkdirResult> {
const pathinfo = pathinfoFor(anypath)
if (! pathinfo.ok) { return pathinfo }
try {
await NodeFSP.mkdir(pathinfo.dirpath, { recursive: true })
return { ...pathinfo, ok: true, gist: 'ok', val: pathinfo }
} catch (err) {
return badOutcome(err as Error, 'fsErr', 'Failed to create directory', { args: anypath }, pathinfo)
}
}
/**
* Given a pathinfo object, assemble the absolute path
*/
export function _abspathForPathparts(pathinfo: Pick<FT.PathinfoT, 'dirpath' | 'barename' | 'fext'>): FT.Abspath {
const dirpathStr = (typeof pathinfo.dirpath === 'string') ? pathinfo.dirpath.trim() : pathinfo.dirpath
const barenameStr = (typeof pathinfo.barename === 'string') ? pathinfo.barename.trim() : pathinfo.barename
if (! dirpathStr || ! barenameStr) { const outcome = badOutcome(new Error('Blank path provided'), 'blankPath', 'Blank path is not a reasonable input', { args: { pathinfo } }); throw outcome.err }
const filename = pathinfo.fext ? `${barenameStr}${pathinfo.fext}` : barenameStr
try {
return PathUtils.resolve(dirpathStr, filename)
} catch (err) {
const outcome = badOutcome(err as Error, 'badPath', 'Failed to resolve path', { args: { pathinfo } })
throw outcome.err
}
}
/**
* Converts a plain pathname to an absolute path
*/
export function _abspathForPathname(pathname: FT.Pathname): FT.Abspath {
const pathnameStr = (typeof pathname === 'string') ? pathname.trim() : pathname
if (! pathnameStr) {
const outcome = badOutcome(new Error('Blank path provided'), 'blankPath', 'Blank path is not a reasonable input', { args: { pathname } })
throw outcome.err
}
try {
return PathUtils.resolve(pathnameStr)
} catch (err) {
const outcome = badOutcome(err as Error, 'badPath', 'Failed to resolve path', { args: { pathname } })
throw outcome.err
}
}
/** Assemble pathinfo using a (possibly relative) dirpath, a barename and a file extension
* @param pathinfo - The pathinfo dna to assemble
* dirpath - The directory path (relative or absolute)
* barename - The base name of the file (without extension)
* fext - The file extension (including the dot)
* @returns A complete pathinfo object, with abspath and dirpath resolved
*/
export function pathinfoFor(anypath: FT.Anypath): FT.PathinfoT | FT.BadFilerResult<'badPath' | 'badInput' | 'blankPath'> {
const pathnameStr = (typeof anypath === 'string') ? anypath.trim() : anypath
if (! pathnameStr) {
return badOutcome(new Error('Blank path provided'), 'blankPath', 'Blank path is not a reasonable input', { args: { anypath } })
}
try {
const abspath = (typeof anypath === 'string') ? _abspathForPathname(anypath) : _abspathForPathparts(anypath)
const dirpath = PathUtils.dirname(abspath)
const basename = PathUtils.basename(abspath)
const fext = PathUtils.extname(basename)
const barename = fext ? basename.slice(0, (- fext.length)) : basename
return {
ok: true,
barename,
fext,
dirpath,
abspath,
}
} catch (err) {
return badOutcome(err as Error, 'badPath', 'Failed to parse path', { anypath })
}
}
/**
* Async generator that reads a file and yields each line
* Returns AsyncGenerator<string, FilerReadResult, unknown>
*/
export async function* starlines(anypath: FT.Anypath): AsyncGenerator<string, FT.FilerReadResult<FT.PathinfoT, FT.CoreReadGist>, unknown> {
const pathinfo = pathinfoFor(anypath)
if (! pathinfo.ok) { return pathinfo }
let fileHandle
try {
fileHandle = await NodeFSP.open(pathinfo.abspath, 'r')
} catch (err) {
return badOutcome(err as Error, 'readErr', 'Issue opening file', { args: anypath }, pathinfo)
}
try {
const buffer = Buffer.alloc(4096)
let leftover = ''
while (true) {
const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, null)
if (bytesRead === 0) { break }
const chunk = leftover + buffer.toString('utf8', 0, bytesRead)
const lines = chunk.split('\n')
leftover = lines.pop()!
// Yield all complete lines except the last one (which might be incomplete)
for (const line of lines) {
try {
yield line
} catch (err) {
const outcome = badOutcome(err as Error, 'callerErr', 'Error processing line', { filepath: pathinfo.abspath, args: anypath })
throw outcome.err
}
}
}
// Yield the final line if there's anything left
if (leftover) {
try {
yield leftover
} catch (err) {
const outcome = badOutcome(err as Error, 'callerErr', 'Error processing final line', { filepath: pathinfo.abspath, args: anypath })
throw outcome.err
}
}
} finally {
await fileHandle.close()
}
return { ...pathinfo, ok: true, gist: 'ok', val: pathinfo }
}
/**
* Creates directory and writes each line from an iterable/async iterable to a file
* Returns FilerResult<PathinfoT>
*/
export async function dumptext(anypath: FT.Anypath, lines: Iterable<string> | AsyncIterable<string>): Promise<FT.FilerWriteResult<FT.PathinfoT, 'writeErr' | 'fsErr' | 'badPath' | 'badInput' | 'blankPath'>> {
const pathinfo = pathinfoFor(anypath)
if (! pathinfo.ok) {return pathinfo }
const mkdirResult = await mkdirp({ ...pathinfo, abspath: pathinfo.dirpath })
if (! mkdirResult.ok) { return mkdirResult }
try {
const fileHandle = await NodeFSP.open(pathinfo.abspath, 'w')
try {
for await (const line of lines) {
await fileHandle.write(line + '\n')
}
} finally {
await fileHandle.close()
}
return { ...pathinfo, ok: true, gist: 'ok', val: pathinfo }
} catch (err) {
return badOutcome(err as Error, 'writeErr', 'Failed to write file', { args: anypath }, pathinfo)
}
}
/**
* Pretty prints JSON and calls dumptext. Returns FilerResult<PathinfoT>
*/
export async function dumpjson(anypath: FT.Anypath, data: any): Promise<FT.FilerWriteResult<FT.PathinfoT, FT.CoreWriteGist>> {
let jsonString: string
try {
jsonString = JSON.stringify(data, null, 2)
} catch (err) {
return badOutcome(err as Error, 'parseErr', 'Failed to stringify data to JSON', { args: anypath })
}
return await dumptext(anypath, [jsonString])
}