json-file-plus
Version:
[![github actions][actions-image]][actions-url] [![coverage][codecov-image]][codecov-url] [![dependency status][deps-svg]][deps-url] [![dev dependency status][dev-deps-svg]][dev-deps-url] [![License][license-image]][license-url] [![Downloads][downloads-im
128 lines (101 loc) • 2.91 kB
JavaScript
import { readFile, writeFile } from 'fs/promises';
import { readFileSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import extend from 'node.extend';
/** @type {(key: unknown) => key is number | string} */
function checkKey(key) {
if ((typeof key !== 'number' || !isFinite(key)) && (typeof key !== 'string' || key.length === 0)) {
throw new TypeError('key must be a finite number or a nonempty string');
}
}
/** @type {(value: unknown) => value is Object} */
function isPlainObject(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
var proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
/** @type {import('.').JSONData} */
export class JSONData {
format;
constructor(raw) {
const hasTrailingNewline = (/\n\n$/).test(raw);
const indentMatch = String(raw).match(/^[ \t]+/m);
const indent = (indentMatch && indentMatch[0]) || 2;
if (raw) {
var result = JSON.parse(raw);
if (result && Object(result) === result) {
this.data = result;
}
}
this.format = {
__proto__: null,
indent,
trailing: hasTrailingNewline,
};
}
async get(key) {
const value = arguments.length > 0 ? this.data[key] : { ...this.data };
return isPlainObject(value) ? { ...value } : value;
}
set(obj) {
if (!isPlainObject(obj)) {
throw new TypeError('object must be a plain object');
}
extend(true, this.data, obj);
}
remove(key) {
checkKey(key);
const status = delete this.data[key];
if (!status) {
throw new Error('deletion failed');
}
}
stringify() {
const { trailing, indent } = this.format;
const endingNewlines = trailing ? '\n\n' : '\n';
return JSON.stringify(this.data, null, indent) + endingNewlines;
}
}
/** @type {import('.').JSONFile} */
export class JSONFile extends JSONData {
#filename;
filename;
constructor(filename, raw) {
super(raw);
this.#filename = filename;
Object.defineProperty(this, 'filename', {
configurable: false,
value: filename,
writable: false,
});
const { format } = this;
delete this.format;
this.format = format;
}
// eslint-disable-next-line no-dupe-class-members -- this is so the function can be borrowed if needed
get filename() {
return this.#filename;
}
async save() {
return writeFile(this.#filename, this.stringify());
}
saveSync() {
writeFileSync(this.#filename, this.stringify());
}
}
/** @type {import('.').sync} */
export function sync(filename) {
const raw = readFileSync(filename, 'utf8');
return new JSONFile(filename, raw);
}
/** @type {import('.').default} */
export default async function readJSON(filename) {
const raw = await readFile(resolve(filename), { encoding: 'utf8' });
return new JSONFile(filename, raw);
}
readJSON.sync = sync;
readJSON.JSONData = JSONData;
readJSON.JSONFile = JSONFile;
export { readJSON as 'module.exports' };