FSTB - working with files in Node.js without pain

When I work with files in Node.js, the thought that I write a lot of the same type of code does not leave me. Creating, reading and writing, moving, deleting, bypassing files and subdirectories, all this is overgrown with an incredible amount of boilerplate, which is further aggravated by the strange names of the module's functions fs



. You can live with all this, but the thought of what can be done more conveniently did not leave me. I wanted such elementary things as, for example, reading or writing text (or json) to a file to be written in one line.





As a result of these thoughts, the FSTB library appeared, in which I tried to improve the ways of interacting with the file system. You can decide whether I succeeded or not by reading this article and trying the library in action.





Background

Working with files in a node takes place in several stages: denial, anger, bargaining ... first we get in some way the path to the file system object, then we check its existence (if necessary), then we work with it. Working with paths in a node is generally moved to a separate module. The coolest function for working with paths is path.join



. A really cool thing that, when I started using it, saved me a bunch of nerve cells.





But there is a problem with paths. The path is a string, although it essentially describes the location of the object in the hierarchical structure. And since we are dealing with an object, why not use the same mechanisms to work with it as when working with ordinary JavaScript objects.





The main problem is that a file system object can have any name from the allowed characters. If I create methods for this object to work with it, then it turns out that, for example, this code: root.home.mydir.unlink



will be ambiguous - what if the directory mydir



has a directory unlink



? And then what? Do I want to delete mydir



or refer to unlink



?





Once I experimented with Javascript Prox and came up with an interesting construction:





const FSPath = function(path: string): FSPathType {
  return new Proxy(() => path, {
    get: (_, key: string) => FSPath(join(path, key)),
  }) as FSPathType;
};
      
      



FSPath



– , , , , Proxy



, FSPath



, . , , :





FSPath(__dirname).node_modules //  path.join(__dirname, "node_modules")
FSPath(__dirname)["package.json"] //  path.join(__dirname, "package.json")
FSPath(__dirname)["node_modules"]["fstb"]["package.json"] //  path.join(__dirname, "node_modules", "fstb", "package.json")

      
      



, , . :





const package_json = FSPath(__dirname).node_modules.fstb["package.json"]
console.log(package_json()) // <  >/node_modules/fstb/package.json
      
      



, , JS. – , , :





FSTB – FileSystem ToolBox.





FSTB:





npm i fstb
      
      



:





const fstb = require('fstb');
      
      



FSPath



, : cwd



, dirname



, home



tmp



( ). envPath



.





:





fstb.cwd["README.md"]().asFile().read.txt().then(txt=>console.log(txt));
      
      



FSTB , async/await:





(async function() {
  const package_json = await fstb.cwd["package.json"]().asFile().read.json();
  console.log(package_json);
})();
      
      



json . , , , .





, - :





const fs = require("fs/promises");
const path = require("path");

(async function() {
  const package_json_path = path.join(process.cwd(), "package.json");
  const file_content = await fs.readFile(package_json_path, "utf8");
  const result = JSON.parse(file_content);
  console.log(result);
})();
      
      



, , , .





. . , Node.js:





const fs = require('fs');
const readline = require('readline');

async function processLineByLine() {
  const fileStream = fs.createReadStream('input.txt');

  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });
  // Note: we use the crlfDelay option to recognize all instances of CR LF
  // ('\r\n') in input.txt as a single line break.

  for await (const line of rl) {
    // Each line in input.txt will be successively available here as `line`.
    console.log(`Line from file: ${line}`);
  }
}
processLineByLine();

      
      



FSTB:





(async function() {
  await fstb.cwd['package.json']()
    .asFile()
    .read.lineByLine()
    .forEach(line => console.log(`Line from file: ${line}`));
})();
      
      



, . , . , , filter



, map



, reduce



.. , , , csv, .map(line => line.split(','))



.





, . . :





(async function() {
  const string_to_write = ' !';
  await fstb.cwd['habr.txt']()
    .asFile()
    .write.txt(string_to_write);
})();
      
      



:





await fstb.cwd['habr.txt']()
    .asFile()
    .write.appendFile(string_to_write, {encoding:"utf8"});
      
      



json:





(async function() {
  const object_to_write = { header: ' !', question: '    ', answer: 42 };
  await fstb.cwd['habr.txt']()
    .asFile()
    .write.json(object_to_write);
})();
      
      



:





(async function() {
  const file = fstb.cwd['million_of_randoms.txt']().asFile();

  //  
  const stream = file.write.createWriteStream();
  stream.on('open', () => {
    for (let index = 0; index < 1_000_000; index++) {
      stream.write(Math.random() + '\n');
    }
    stream.end();
  });
  await stream;

  //  
  const lines = await file.read.lineByLine().reduce(acc => ++acc, 0);
  console.log(`${lines} lines count`);
})();
      
      



, ? :





await stream; // <= WTF?!!
      
      



, WriteStream



, . , , , await



. , await



.





, , . FSTB? , fs.





:





const stat = await file.stat()
console.log(stat);
      
      



:





  Stats {
    dev: 1243191443,
    mode: 33206,
    nlink: 1,
    uid: 0,
    gid: 0,
    rdev: 0,
    blksize: 4096,
    ino: 26740122787869450,
    size: 19269750,
    blocks: 37640,
    atimeMs: 1618579566188.5884,
    mtimeMs: 1618579566033.8242,
    ctimeMs: 1618579566033.8242,
    birthtimeMs: 1618579561341.9297,
    atime: 2021-04-16T13:26:06.189Z,
    mtime: 2021-04-16T13:26:06.034Z,
    ctime: 2021-04-16T13:26:06.034Z,
    birthtime: 2021-04-16T13:26:01.342Z
 }
      
      



-:





const fileHash = await file.hash.md5();

console.log("File md5 hash:", fileHash);
// File md5 hash: 5a0a221c0d24154b850635606e9a5da3
      
      



:





const renamedFile = await file.rename(`${fileHash}.txt`);
      
      



:





//   ,       
//     "temp"    
const targetDir = renamedFile.fsdir.fspath.temp().asDir()
if(!(await targetDir.isExists())) await targetDir.mkdir()
  
// 
const fileCopy = await renamedFile.copyTo(targetDir)
  
const fileCopyHash = await fileCopy.hash.md5();

console.log("File copy md5 hash:", fileCopyHash);
// File md5 hash: 5a0a221c0d24154b850635606e9a5da3
      
      



:





await renamedFile.unlink();
      
      



, , :





console.log({ 
    isExists: await file.isExists(), 
    isReadable: await file.isReadable(), 
    isWritable: await file.isWritable() });
      
      



, , , .





:

, – . , . , FSTB . FSDir



, :





//  FSDir  node_modules:
const node_modules = fstb.cwd.node_modules().asDir();
      
      



? -, :





//      
await node_modules.subdirs().forEach(async dir => console.log(dir.name));
      
      



filter, map, reduce, forEach, toArray. , , Β«@Β» .





const ileSizes = await node_modules
  .subdirs()
  .filter(async dir => dir.name.startsWith('@'))
  .map(async dir => ({ name: dir.name, size: await dir.totalSize() })).toArray();

fileSizes.sort((a,b)=>b.size-a.size);
console.table(fileSizes);
      
      



- :





β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ (index) β”‚         name         β”‚  size   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚    0    β”‚       '@babel'       β”‚ 6616759 β”‚
β”‚    1    β”‚ '@typescript-eslint' β”‚ 2546010 β”‚
β”‚    2    β”‚       '@jest'        β”‚ 1299423 β”‚
β”‚    3    β”‚       '@types'       β”‚ 1289380 β”‚
β”‚    4    β”‚   '@webassemblyjs'   β”‚ 710238  β”‚
β”‚    5    β”‚      '@nodelib'      β”‚ 512000  β”‚
β”‚    6    β”‚      '@rollup'       β”‚ 496226  β”‚
β”‚    7    β”‚       '@bcoe'        β”‚ 276877  β”‚
β”‚    8    β”‚       '@xtuc'        β”‚ 198883  β”‚
β”‚    9    β”‚    '@istanbuljs'     β”‚  70704  β”‚
β”‚   10    β”‚      '@sinonjs'      β”‚  37264  β”‚
β”‚   11    β”‚     '@cnakazawa'     β”‚  25057  β”‚
β”‚   12    β”‚    '@size-limit'     β”‚  14831  β”‚
β”‚   13    β”‚       '@polka'       β”‚  6953   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
      
      



, , ))





. , typescript . , :





const ts_versions = await node_modules
  .subdirs()
  .map(async dir => ({
    dir,
    package_json: dir.fspath['package.json']().asFile(),
  }))
  //  package.json  
  .filter(async ({ package_json }) => await package_json.isExists())
  //  package.json
  .map(async ({ dir, package_json }) => ({
    dir,
    content: await package_json.read.json(),
  }))
  //  devDependencies.typescript  package.json
  .filter(async ({ content }) => content.devDependencies?.typescript)
  //      typescript
  .map(async ({ dir, content }) => ({
    name: dir.name,
      ts_version: content.devDependencies.typescript,
    }))
    .toArray();

  console.table(ts_versions);
      
      



:





  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ (index) β”‚            name             β”‚      ts_version       β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚    0    β”‚            'ajv'            β”‚       '^3.9.5'        β”‚
  β”‚    1    β”‚         'ast-types'         β”‚        '3.9.7'        β”‚
  β”‚    2    β”‚         'axe-core'          β”‚       '^3.5.3'        β”‚
  β”‚    3    β”‚         'bs-logger'         β”‚         '3.x'         β”‚
  β”‚    4    β”‚           'chalk'           β”‚       '^2.5.3'        β”‚
  β”‚    5    β”‚    'chrome-trace-event'     β”‚       '^2.8.1'        β”‚
  β”‚    6    β”‚         'commander'         β”‚       '^3.6.3'        β”‚
  β”‚    7    β”‚      'constantinople'       β”‚       '^2.7.1'        β”‚
  β”‚    8    β”‚         'css-what'          β”‚       '^4.0.2'        β”‚
  β”‚    9    β”‚         'deepmerge'         β”‚       '=2.2.2'        β”‚
  β”‚   10    β”‚         'enquirer'          β”‚       '^3.1.6'        β”‚
...
      
      



?





. fspath:





//  FSDir  node_modules:
const node_modules = fstb.cwd.node_modules().asDir();
//      "package.json"   "fstb"
const package_json = node_modules.fspath.fstb["package.json"]().asFile()
      
      



, temp . FSTB mkdtemp



.





mkdir



. copyTo



moveTo



. - rmdir



( ) rimraf



( ).





:





//   
const temp_dir = await fstb.mkdtemp("fstb-");
if(await temp_dir.isExists()) console.log("  ")
//     : src, target1  target2
const src = await temp_dir.fspath.src().asDir().mkdir();
const target1 = await temp_dir.fspath.target1().asDir().mkdir();
const target2 = await temp_dir.fspath.target2().asDir().mkdir();

//  src   :
const test_txt = src.fspath["test.txt"]().asFile();
await test_txt.write.txt(", !");
  
//  src  target1
const src_copied = await src.copyTo(target1);
//  src  target2
const src_movied = await src.moveTo(target2);

//    
// subdirs(true) –     
await temp_dir.subdirs(true).forEach(async dir=>{
  await dir.files().forEach(async file=>console.log(file.path))
})

//   ,     
console.log(await src_copied.fspath["test.txt"]().asFile().read.txt())
console.log(await src_movied.fspath["test.txt"]().asFile().read.txt())

//      
await temp_dir.rimraf()
if(!(await temp_dir.isExists())) console.log("  ")
      
      



:





  
C:\Users\debgger\AppData\Local\Temp\fstb-KHT0zv\target1\src\test.txt
C:\Users\debgger\AppData\Local\Temp\fstb-KHT0zv\target2\src\test.txt
, !
, !
  
      
      



, , . , join’ , .





, Node.js. , . FSTB . , , , , .





, FSTB, :













  • .





  • , IDE .





  • ,





  • Node.js 10- ,





, , , FSPath, , , . .





, , . , . , , .





GitHub: https://github.com/debagger/fstb





: https://debagger.github.io/fstb/





Thank you for attention!








All Articles