@freeword/meta
Version:
Meta package for Freeword: exports all core types, constants, and utilities from the src/ directory.
380 lines • 14.4 kB
JavaScript
import _ /**/ from 'lodash';
import readline from 'node:readline';
import { Readable } from 'node:stream';
import * as Zlib from 'node:zlib';
import NodeFS from 'node:fs';
import NodeFSP from 'node:fs/promises';
import PathUtils from 'node:path';
import * as UF from "../utils/UF.js";
import { throwable } from "../utils/OutcomeUtils.js";
function badOutcome(err, gist, preambleMsg, tmi, pathinfo) {
const origmsg = err.message;
const extError = new Error(`${preambleMsg}: ${origmsg}`);
const errTMI = {
...(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) {
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, 'fsErr', 'Failed to create directory', { args: anypath }, pathinfo);
}
}
/**
* Given a pathinfo object, assemble the absolute path
*/
export function _abspathForPathparts(pathinfo, ...pathsegs) {
if (!_.isEmpty(pathsegs)) {
const outcome = badOutcome(new Error('Cannot have path object and path segments'), 'badInput', 'Path segments are not a reasonable input', { args: { pathinfo, pathsegs } });
throw outcome.err;
}
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, 'badPath', 'Failed to resolve path', { args: { pathinfo } });
throw outcome.err;
}
}
/**
* Converts a plain pathname to an absolute path
*/
export function _abspathForPathname(pathname, ...pathsegs) {
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, ...UF.scrubNil(pathsegs));
}
catch (err) {
const outcome = badOutcome(err, '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)
* @param pathsegs - Additional path segments to append to the pathname
* @returns A complete pathinfo object, with abspath and dirpath resolved
*/
export function pathinfoFor(anypath, ...pathsegs) {
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, ...pathsegs) : _abspathForPathparts(anypath, ...pathsegs);
const dirpath = PathUtils.dirname(abspath);
const basename = PathUtils.basename(abspath);
const fext = PathUtils.extname(basename).slice(1);
const barename = fext ? basename.slice(0, (-1 - fext.length)) : basename;
return {
ok: true,
barename,
basename,
fext,
dirpath,
abspath,
};
}
catch (err) {
return badOutcome(err, 'badPath', 'Failed to parse path', { anypath });
}
}
export function dirpathFor(anypath, ...pathsegs) { return pathinfoFor(anypath, ...pathsegs).dirpath; }
export function abspathFor(anypath, ...pathsegs) { return pathinfoFor(anypath, ...pathsegs).abspath; }
export function barenameFor(anypath, ...pathsegs) { return pathinfoFor(anypath, ...pathsegs).barename; }
export function fextFor(anypath, ...pathsegs) { return pathinfoFor(anypath, ...pathsegs).fext; }
export function __dirname(importMetaURL, ...relpaths) {
const callerpath = String(importMetaURL).replace(/^file:\/\//, '/');
const pathinfo = pathinfoFor(PathUtils.dirname(callerpath), ...relpaths);
if (!pathinfo.ok) {
throw pathinfo.err;
}
return pathinfo.abspath;
}
export function __relname(importMetaURL, ...relpaths) {
return __dirname(importMetaURL, ...relpaths);
}
/**
* Async generator that reads a file and yields each line
* Returns AsyncGenerator<string, FilerReadResult, unknown>
*/
export async function* starlinesFiddly(anypath) {
const pathinfo = pathinfoFor(anypath);
if (!pathinfo.ok) {
return pathinfo;
}
let fileHandle;
try {
fileHandle = await NodeFSP.open(pathinfo.abspath, 'r');
}
catch (err) {
return badOutcome(err, '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, '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, '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 };
}
function _openRawFilestream(anypath, { encoding = 'utf8' } = {}) {
const pathinfo = pathinfoFor(anypath);
if (!pathinfo.ok) {
return pathinfo;
}
try {
NodeFS.accessSync(pathinfo.abspath, NodeFS.constants.R_OK);
}
catch (rawerr) {
const err = rawerr;
let linkExists = false;
try {
NodeFS.lstatSync(pathinfo.abspath);
linkExists = true;
}
catch { /* path does not exist */ }
if (linkExists) {
return badOutcome(err, 'readErr', 'Issue opening file: filesystem link is incorrect', { args: anypath }, pathinfo);
}
return badOutcome(err, 'fileNotFound', `Path ${pathinfo.abspath} is absent`, { args: anypath }, pathinfo);
}
try {
const contentsStream = NodeFS.createReadStream(pathinfo.abspath, { encoding: encoding ?? undefined });
return { ...pathinfo, gist: 'ok', ok: true, val: contentsStream };
}
catch (rawerr) {
const err = rawerr;
if (err.code === 'ENOENT') {
return badOutcome(err, 'fileNotFound', `Path ${pathinfo.abspath} is absent`, { args: anypath }, pathinfo);
}
return badOutcome(err, 'readErr', 'Issue opening file', { args: anypath }, pathinfo);
}
}
/** Opens a raw filestream with the (decompressed) file contents
* @param anypath - The pathname or pathinfo of the file to open. If it ends in `.gz` or `.bz2` it will be decompressed
* @returns A Readable stream or a BadFilerResult
*/
export function openFilestream(anypath) {
const pathinfo = pathinfoFor(anypath);
if (!pathinfo.ok) {
return pathinfo;
}
const encoding = /^(gz|bz2|zip)$/.test(pathinfo.fext) ? null : 'utf8';
const contentsStream = _openRawFilestream(pathinfo, { encoding });
if (!contentsStream.ok) {
return contentsStream;
}
if (contentsStream.fext === 'gz') {
return { ...contentsStream, val: contentsStream.val.pipe(Zlib.createGunzip()) };
}
if (contentsStream.fext === 'zip') {
return { ...contentsStream, val: contentsStream.val.pipe(Zlib.createUnzip()) };
}
return contentsStream;
}
export function openLinestream(anypath) {
const contentsStream = openFilestream(anypath);
if (!contentsStream.ok) {
return contentsStream;
}
return { ...contentsStream, val: readline.createInterface({ input: contentsStream.val }) };
}
/** Loads the contents of a file, returning either { ok: true, val: stringContentsOfThatFile, ...pathinfo }, or a BadFilerResult
* Like the other filer methods, it never throws: consult result.ok for a tagged union
*/
export async function loadtext(anypath) {
const contentsStream = openFilestream(anypath);
if (!contentsStream.ok) {
return contentsStream;
}
const contents = await UF.slurp(contentsStream.val);
return { ...contentsStream, val: contents.join('') };
}
/**
* Async generator that reads a file and yields each line
* Returns AsyncGenerator<string, FilerReadResult, unknown>
*/
export async function* starlines(anypath) {
const linestream = openLinestream(anypath);
if (!linestream.ok) {
throw linestream.err;
}
let lineNumber = 0;
try {
for await (const line of linestream.val) {
lineNumber += 1;
try {
yield line;
}
catch (err) {
throw throwable(`Upstream error at line ${lineNumber}`, 'callerErr', { line, lineNumber, filepath: linestream.abspath, args: anypath }, err);
}
}
}
catch (err) {
if (err.extensions?.gist === 'consumeErr') {
throw err;
}
if (err.code === 'ENOENT') {
throw throwable(`Path ${linestream.abspath} is absent`, 'fileNotFound', { lineNumber, filepath: anypath, args: { anypath } }, err);
}
if (err.code === 'Z_DATA_ERROR') {
throw throwable(`Decompression error ${anypath}:${lineNumber}`, 'compressErr', { lineNumber, filepath: anypath, args: { anypath } }, err);
}
throw throwable(`File read error ${anypath}:${lineNumber}`, 'consumeErr', { lineNumber, filepath: anypath, args: { anypath } }, err);
}
finally {
if (linestream.val) {
linestream.val.close();
}
}
return { ...linestream, val: lineNumber };
}
const JSONKV_RE = /^\s*(?:([\}\]])\s*$|[\{\[,]\s*\t\s*("[^\t]*)\t\s*:\s*\t\s*(.*)|[\{\[,]\s*\t\s*(.*))$/;
export async function* starjsonEntries(anypath) {
let val;
let key;
let lineNumber = 0;
const pathinfo = pathinfoFor(anypath);
if (!pathinfo.ok) {
throw pathinfo.err;
}
for await (const line of starlines(pathinfo.abspath)) {
lineNumber += 1;
const match = JSONKV_RE.exec(line);
if (match?.[1]) {
continue;
}
const jskey = match?.[2];
const json = match?.[3] ?? match?.[4] ?? line;
try {
key = jskey ? JSON.parse(jskey) : (lineNumber - 1);
val = JSON.parse(json);
}
catch (err) {
throw throwable(`Failed to parse JSON at line ${lineNumber}`, 'parseErr', { line, lineNumber, filepath: anypath, args: { anypath } }, err);
}
try {
yield [key, val, (lineNumber - 1)];
}
catch (err) {
throw throwable(`Upstream error at line ${lineNumber}`, 'consumeErr', { val, line, lineNumber, filepath: anypath, args: { anypath } }, err);
}
}
return { ...pathinfo, ok: true, gist: 'ok', val: lineNumber };
}
export async function* starjsonl(anypath) {
for await (const [_key, val, _lineNumber] of starjsonEntries(anypath)) {
yield val;
}
}
export async function* starjsonkeys(anypath) {
for await (const [key, _val, _lineNumber] of starjsonEntries(anypath)) {
yield key;
}
}
/**
* Creates directory and writes each line from an iterable/async iterable to a file
* Returns FilerResult<PathinfoT>
*/
export async function dumptext(anypath, lines) {
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, 'writeErr', 'Failed to write file', { args: anypath }, pathinfo);
}
}
/**
* Pretty prints JSON and calls dumptext. Returns FilerResult<PathinfoT>
*/
export async function dumpjson(anypath, data) {
let jsonString;
try {
jsonString = JSON.stringify(data, null, 2);
}
catch (err) {
return badOutcome(err, 'parseErr', 'Failed to stringify data to JSON', { args: anypath });
}
return await dumptext(anypath, [jsonString]);
}
//# sourceMappingURL=Filer.js.map