quaff
Version:
Collect JS/JSON/YAML/YML/CSV/TSV/ArchieML files from a source folder and convert them into a single object.
108 lines (107 loc) • 3.49 kB
JavaScript
// internal
import { promises as fs } from 'fs';
import path from 'path';
import { pathToFileURL } from 'url';
// packages
import archieml from 'archieml';
import { dset } from 'dset';
import { csvParse, tsvParse } from 'd3-dsv';
import parseJson from 'parse-json';
import { totalist } from 'totalist';
import yaml from 'js-yaml';
/**
* quaff's valid extensions.
*/
const validExtensions = [
'.js',
'.cjs',
'.mjs',
'.json',
'.yaml',
'.yml',
'.csv',
'.tsv',
'.aml',
];
/**
* @param filePath the input file path
* @returns {Promise<unknown>}
*/
export async function loadFile(filePath) {
const ext = path.extname(filePath);
// it can return absolutely anything
let data;
// we give JavaScript entries a special treatment
if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
// js path
// @ts-ignore - dynamic imports *can* take URL objects, but TypeScript disagrees
data = (await import(pathToFileURL(filePath))).default;
if (typeof data === 'function') {
data = await data();
}
}
else {
// otherwise look for matches
const fileContents = await fs.readFile(filePath, 'utf8');
switch (ext) {
// json path
case '.json':
data = parseJson(fileContents, filePath);
break;
// yaml paths
case '.yaml':
case '.yml':
data = yaml.load(fileContents, { filename: filePath });
break;
// csv path
case '.csv':
data = csvParse(fileContents);
break;
// tsv path
case '.tsv':
data = tsvParse(fileContents);
break;
// aml path
case '.aml':
data = archieml.load(fileContents);
break;
default:
throw new Error(`Unable to parse ${filePath} - no valid processor found for ${ext} extension`);
}
}
return data;
}
/**
* @param dirPath the input directory
* @returns {Promise<LoadReturnValue>}
*/
export async function load(dirPath) {
// normalize the input path
const cwd = path.normalize(dirPath);
// the object we will eventually return with data
const output = {};
// a set to watch out for duplicate keys
const existing = new Set();
// loop through the files in the directory
await totalist(cwd, async (rel, abs) => {
const { name, dir, ext } = path.parse(rel);
// early exit if not a valid extension
if (!validExtensions.includes(ext))
return;
// remove the leading path, split into a list, and filter out empty strings
const dirs = dir.split(path.sep).filter(Boolean);
// add the filename to the path part list
dirs.push(name);
// build a unique "key" for this file so we can watch out for dupes
const key = dirs.join('.');
// if this key isn't unique, throw an error
if (existing.has(key)) {
throw new Error(`More than one file attempted to use "${key}" as its path. This error is caused by having multiple files in a directory with the same name but different extensions.`);
}
// otherwise save it for checking future inputs
existing.add(key);
const data = await loadFile(abs);
dset(output, dirs, data);
});
return output;
}