UNPKG

@naturalcycles/nodejs-lib

Version:
273 lines (266 loc) 8.14 kB
/* 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' : ''); }