@naturalcycles/nodejs-lib
Version:
Standard library for Node.js
273 lines (266 loc) • 8.14 kB
JavaScript
/*
Why?
Convenience re-export/re-implementation of most common fs functions from
node:fs, node:fs/promises and fs-extra
Defaults to string input/output, as it's used in 80% of time. For the rest - you can use native fs.
Allows to import it easier, so you don't have to choose between the 3 import locations.
That's why function names are slightly renamed, to avoid conflict.
Credit to: fs-extra (https://github.com/jprichardson/node-fs-extra)
*/
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import path from 'node:path';
import { _jsonParse } from '@naturalcycles/js-lib/string/json.util.js';
/**
* fs2 conveniently groups filesystem functions together.
* Supposed to be almost a drop-in replacement for these things together:
*
* 1. node:fs
* 2. node:fs/promises
* 3. fs-extra
*/
class FS2 {
// Naming convention is:
// - async function has Async in the name, e.g readTextAsync
// - sync function has postfix in the name, e.g readText
/**
* Convenience wrapper that defaults to utf-8 string output.
*/
readText(filePath) {
return fs.readFileSync(filePath, 'utf8');
}
/**
* Convenience wrapper that defaults to utf-8 string output.
*/
async readTextAsync(filePath) {
return await fsp.readFile(filePath, 'utf8');
}
readBuffer(filePath) {
return fs.readFileSync(filePath);
}
async readBufferAsync(filePath) {
return await fsp.readFile(filePath);
}
readJson(filePath) {
const str = fs.readFileSync(filePath, 'utf8');
return _jsonParse(str);
}
async readJsonAsync(filePath) {
const str = await fsp.readFile(filePath, 'utf8');
return _jsonParse(str);
}
writeFile(filePath, data) {
fs.writeFileSync(filePath, data);
}
async writeFileAsync(filePath, data) {
await fsp.writeFile(filePath, data);
}
writeJson(filePath, data, opt) {
const str = stringify(data, opt);
fs.writeFileSync(filePath, str);
}
async writeJsonAsync(filePath, data, opt) {
const str = stringify(data, opt);
await fsp.writeFile(filePath, str);
}
appendFile(filePath, data) {
fs.appendFileSync(filePath, data);
}
async appendFileAsync(filePath, data) {
await fsp.appendFile(filePath, data);
}
outputJson(filePath, data, opt) {
const str = stringify(data, opt);
this.outputFile(filePath, str);
}
async outputJsonAsync(filePath, data, opt) {
const str = stringify(data, opt);
await this.outputFileAsync(filePath, str);
}
outputFile(filePath, data) {
const dirPath = path.dirname(filePath);
if (!fs.existsSync(dirPath)) {
this.ensureDir(dirPath);
}
fs.writeFileSync(filePath, data);
}
async outputFileAsync(filePath, data) {
const dirPath = path.dirname(filePath);
if (!(await this.pathExistsAsync(dirPath))) {
await this.ensureDirAsync(dirPath);
}
await fsp.writeFile(filePath, data);
}
pathExists(filePath) {
return fs.existsSync(filePath);
}
async pathExistsAsync(filePath) {
try {
await fsp.access(filePath);
return true;
}
catch {
return false;
}
}
requireFileToExist(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`Required file should exist: ${filePath}`);
}
}
ensureDir(dirPath) {
fs.mkdirSync(dirPath, {
mode: 0o777,
recursive: true,
});
}
async ensureDirAsync(dirPath) {
await fsp.mkdir(dirPath, {
mode: 0o777,
recursive: true,
});
}
ensureFile(filePath) {
let stats;
try {
stats = fs.statSync(filePath);
}
catch { }
if (stats?.isFile())
return;
const dir = path.dirname(filePath);
try {
if (!fs.statSync(dir).isDirectory()) {
// parent is not a directory
// This is just to cause an internal ENOTDIR error to be thrown
fs.readdirSync(dir);
}
}
catch (err) {
// If the stat call above failed because the directory doesn't exist, create it
if (err?.code === 'ENOENT') {
this.ensureDir(dir);
return;
}
throw err;
}
fs.writeFileSync(filePath, '');
}
async ensureFileAsync(filePath) {
let stats;
try {
stats = await fsp.stat(filePath);
}
catch { }
if (stats?.isFile())
return;
const dir = path.dirname(filePath);
try {
if (!(await fsp.stat(dir)).isDirectory()) {
// parent is not a directory
// This is just to cause an internal ENOTDIR error to be thrown
await fsp.readdir(dir);
}
}
catch (err) {
// If the stat call above failed because the directory doesn't exist, create it
if (err?.code === 'ENOENT')
return await this.ensureDirAsync(dir);
throw err;
}
await fsp.writeFile(filePath, '');
}
removePath(fileOrDirPath, opt) {
fs.rmSync(fileOrDirPath, { recursive: true, force: true, ...opt });
}
async removePathAsync(fileOrDirPath, opt) {
await fsp.rm(fileOrDirPath, { recursive: true, force: true, ...opt });
}
emptyDir(dirPath) {
let items;
try {
items = fs.readdirSync(dirPath);
}
catch {
this.ensureDir(dirPath);
return;
}
for (const item of items) {
this.removePath(path.join(dirPath, item));
}
}
async emptyDirAsync(dirPath) {
let items;
try {
items = await fsp.readdir(dirPath);
}
catch {
return await this.ensureDirAsync(dirPath);
}
await Promise.all(items.map(item => this.removePathAsync(path.join(dirPath, item))));
}
/**
* Cautious, underlying Node function is currently Experimental.
*/
copyPath(src, dest, opt) {
fs.cpSync(src, dest, {
recursive: true,
...opt,
});
}
/**
* Cautious, underlying Node function is currently Experimental.
*/
async copyPathAsync(src, dest, opt) {
await fsp.cp(src, dest, {
recursive: true,
...opt,
});
}
renamePath(src, dest) {
fs.renameSync(src, dest);
}
async renamePathAsync(src, dest) {
await fsp.rename(src, dest);
}
movePath(src, dest, opt) {
this.copyPath(src, dest, opt);
this.removePath(src);
}
async movePathAsync(src, dest, opt) {
await this.copyPathAsync(src, dest, opt);
await this.removePathAsync(src);
}
/**
* Returns true if the path is a directory.
* Otherwise returns false.
* Doesn't throw, returns false instead.
*/
isDirectory(filePath) {
return (fs2
.stat(filePath, {
throwIfNoEntry: false,
})
?.isDirectory() || false);
}
// Re-export the whole fs/fsp, for the edge cases where they are needed
fs = fs;
fsp = fsp;
// Re-export existing fs/fsp functions
// rm/rmAsync are replaced with removePath/removePathAsync
lstat = fs.lstatSync;
lstatAsync = fsp.lstat;
stat = fs.statSync;
statAsync = fsp.stat;
mkdir = fs.mkdirSync;
mkdirAsync = fsp.mkdir;
readdir = fs.readdirSync;
readdirAsync = fsp.readdir;
createWriteStream = fs.createWriteStream;
createReadStream = fs.createReadStream;
}
export const fs2 = new FS2();
function stringify(data, opt) {
// If pretty-printing is enabled (spaces) - also add a newline at the end (to match our prettier config)
return JSON.stringify(data, null, opt?.spaces) + (opt?.spaces ? '\n' : '');
}