UNPKG

@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
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]) }