node-treeview

Asynchronous filesystem tree view for node.

Usage no npm install needed!

<script type="module">
  import nodeTreeview from 'https://cdn.skypack.dev/node-treeview';
</script>

README

node-treeview

Asynchronous filesystem tree view for node.

Build Status

Javascript (quick start)

Usage

const { TreeView } = require('node-treeview');

// Using callback
new TreeView(options).process(path, (error, tree) => {
  if (tree) {
    // do some stuff...
  } else {
    // handle error...
  }
});

// Using Promise
new TreeView(options).process(path).then(tree => {
  // do some stuff...
}).catch(error => {
  // handle error...
});

// Using async/await
async function getTree() {
  let tree;
  try {
    tree = await new TreeView(options).process(path);
  } catch (error) {
    // handle error...
  }
  // do some stuff...
}
getTree();

Example

const { TreeView } = require('node-treeview');

new TreeView({ content: true, depth: 2 })
  .process('path/to/dir')
  .then(tree => console.log(tree));

Here is what the json output looks like:

[{
  "name": "file1.txt",
  "path": "path/to/dir",
  "pathname": "path/to/dir/file1.txt",
  "depth": 0,
  "created": "2017-10-23T18:29:28.000Z",
  "modified": "2017-10-23T18:29:28.000Z",
  "type": "file",
  "size": 13,
  "ext": "txt",
  "binary": false,
  "content": "file1 content"
}, {
  "name": "subdir",
  "path": "path/to/dir",
  "pathname": "path/to/dir/subdir",
  "depth": 0,
  "created": "2017-10-22T10:48:48.000Z",
  "modified": "2017-10-23T18:29:29.000Z",
  "type": "dir",
  "nodes": [{
    "name": "file2.txt",
    "path": "path/to/dir/subdir",
    "pathname": "path/to/dir/subdir/file2.txt",
    "depth": 1,
    "created": "2017-10-23T18:29:28.000Z",
    "modified": "2017-10-23T18:29:29.000Z",
    "type": "file",
    "size": 13,
    "ext": "txt",
    "binary": false,
    "content": "file3 content"
  }, {
    "name": "logo.png",
    "path": "path/to/dir/subdir",
    "pathname": "path/to/dir/subdir/logo.png",
    "depth": 1,
    "created": "2017-10-23T18:29:29.000Z",
    "modified": "2017-10-23T18:29:29.000Z",
    "type": "file",
    "size": 325,
    "ext": "png",
    "binary": true,
    "content": "iVBORw0KGgoAAAANSUh..." //-> base64
  }]
}]

The TreeView lets you listen to item events.

const { TreeView } = require('node-treeview');

new TreeView()
  .on('item', data => console.log(`${data.type}: ${data.pathname}`))
  .process('path/to/dir')
  .then(() => console.log('done!'));

Here is what the txt output looks like:

file: path/to/dir/file1.txt
dir: path/to/dir/subdir
file: path/to/dir/subdir/file2.txt
file: path/to/dir/subdir/logo.png
done!

Emitted file never have content property, emitted dir always have nodes property equal to an empty array.

The TreeView lets you process trees in parallel.

const { TreeView } = require('node-treeview');

const treeView = new TreeView({
  relative: true
}).on('item', (data, ctx) => {
  // Listen to each emitted data in its own context
  // ('path/to/dir1' or 'path/to/dir2')
  console.log(`${ctx.rootPath} -> ${data.pathname}`);
});

Promise.all[
  // Use the same TreeView instance to process different
  // paths in parallel with the same options
  treeView.process('path/to/dir1'),
  treeView.process('path/to/dir2')
].then(([tree1, tree2]) => {
  console.log(tree1);
  console.log(tree2);
});

The TreeView lets you watch the filesystem.

const { TreeView } = require('node-treeview');

const treeView = new TreeView();

treeview
  .on('item'), (item) => {/* Item emitted (discovered, added, modified or removed) */})

  .on('ready'), (tree) => {/* Initial tree available */})

  .on('add'), (item) => {/* Item added */})
  .on('change'), (item) => {/* Item modified */})
  .on('unlink'), (item) => {/* Item removed */})

  .on('tree'), (tree) => {/* Refreshed tree available (after 'add', 'change' or 'unlink' event) */})

  .on('all'), (event, data) => {/* Listen to all events */});

// Start watching
const watcher = treeview.watch('path/to/dir');

// Stop watching after 1mn
setTimeout(watcher.close, 60000);

Under the hood, the watch feature is provided by fs.watch on Mac and Windows, and chokidar on other platforms.

You should NOT process trees in parallel when you watch the filesystem. Otherwise the watch method will not work properly.

TypeScript

Interface overview

// Basic interface of files and directories (used for unreadable resource)
export interface IRef {
  name: string;
  path: string;
  pathname: string;
  depth: number;
  error?: any;
}

export interface IFile extends IRef {
  type: 'file';
  created: Date;
  modified: Date;
  size: number;
  ext: string;
  binary: boolean;
  content?: string;
}

export interface IDir extends IRef {
  type: 'dir';
  created: Date;
  modified: Date;
  nodes: TreeNode[];
}

// The final output is of type: `TreeNode[]`
// and the `TreeView.process` method returns a `Promise<TreeNode[]>`
export type TreeNode = IFile | IDir | IRef;

// List of emitted events
export type Event
  = 'item'
  | 'ready'
  | 'tree'
  | 'add'
  | 'change'
  | 'unlink'
  | 'all';

Options

export interface IOptsParam {
  // Include hidden files in output
  all?: boolean;
  // Add files content to output
  content?: boolean;
  // Maximum depth of directories
  depth?: number;
  // Use relative path
  relative?: boolean;
  // List of directory paths to include in output
  include?: string[];
  // List of directory paths to exclude from output
  exclude?: string[];
  // Match files based on glob pattern
  glob?: string[];
  // Sort output
  sort?: Sorting;
}

// Tree sort type
export enum Sorting {
  Alpha,
  FileFirst,
  DirFirst
}

Using typing

import { TreeView } from 'node-treeview';
import * as Model from 'node-treeview/model'

const options: Model.IOptsParam = { depth: 2 };
const path = 'path/to/dir';

const promise: Promise<TreeNode[]> =
  new TreeView(options).process(path);

promise.then(tree => {
  tree.forEach(item => {
    if ((item as Model.IRef).error) {
      // handle error...
    } else if ((item as Model.IDir).type === 'dir') {
      // handle directory...
    } else if ((item as Model.IFile).type === 'file') {
      // handle file...
    }
  });
});

Helper

flatten

The flatten helper lets you get a flat version of the tree.

import { TreeView } from 'node-treeview';
import { flatten } from 'node-treeview/helper';

new TreeView().process('path/to/dir').then(tree => {
  const flat = flatten(tree);
  console.log(flat);
});

Or for JavaScript style using require:

const { TreeView } = require('node-treeview');
const { flatten } = require('node-treeview/helper');
// ...

Here is what the json output looks like:

[{
  "name": "file1.txt",
  "path": "path/to/dir",
  "pathname": "path/to/dir/file1.txt",
  "depth": 0,
  "created": "2017-10-23T18:29:28.000Z",
  "modified": "2017-10-23T18:29:28.000Z",
  "type": "file",
  "size": 13,
  "ext": "txt",
  "binary": false
}, {
  "name": "file2.txt",
  "path": "path/to/dir/subdir",
  "pathname": "path/to/dir/subdir/file2.txt",
  "depth": 1,
  "created": "2017-10-23T18:29:28.000Z",
  "modified": "2017-10-23T18:29:29.000Z",
  "type": "file",
  "size": 13,
  "ext": "txt",
  "binary": false
}, {
  "name": "logo.png",
  "path": "path/to/dir/subdir",
  "pathname": "path/to/dir/subdir/logo.png",
  "depth": 1,
  "created": "2017-10-23T18:29:29.000Z",
  "modified": "2017-10-23T18:29:29.000Z",
  "type": "file",
  "size": 325,
  "ext": "png",
  "binary": true
}]

clean

The clean helper lets you clean empty directories from the tree.

import { TreeView } from 'node-treeview';
import { clean } from 'node-treeview/helper';

new TreeView().process('path/to/dir').then(tree => {
  const cleaned = clean(tree);
  console.log(cleaned);
});

Or for JavaScript style using require:

const { TreeView } = require('node-treeview');
const { clean } = require('node-treeview/helper');
// ...

pretty

The pretty helper lets you pretty-print the tree.

import { TreeView } from 'node-treeview';
import { pretty } from 'node-treeview/helper';

new TreeView().process('path/to/dir').then(tree => {
  console.log(pretty(tree));
});

Here is what the txt output looks like:

├─ fruits
│  ├─ apple.txt
│  └─ pears.txt
└─ vegetables
   ├─ bean.txt
   ├─ potato.txt
   └─ endive.txt

With the pretty helper you have full control over how to render the tree.

import { TreeView } from 'node-treeview';
import { pretty } from 'node-treeview/helper';

new TreeView().process('path/to/dir').then(tree => {
  console.log(
    pretty(tree, (box: string, item: Model.TreeNode) => {
      if ((item as Model.IDir).type === 'dir') {
        return box + `(${item.name})`;
      } else if ((item as Model.IFile).type === 'file') {
        return box + item.name + ` [${(item as Model.IFile).size} bytes]`;
      } else {
        return box + item.name;
      }
    })
  );
});

Here is what the txt output looks like:

├─ (fruits)
│  ├─ apple.txt [51 bytes]
│  └─ pears.txt [24 bytes]
└─ (vegetables)
   ├─ bean.txt [13 bytes]
   ├─ potato.txt [87 bytes]
   └─ endive.txt [69 bytes]

And for quick rendering, use the predefined renderer functions.

import { TreeView } from 'node-treeview';
import { pretty } from 'node-treeview/helper';
import { renderer } from 'node-treeview/helper/pretty';

new TreeView().process('path/to/dir').then(tree => {
  console.log(pretty(tree, renderer.light));
  console.log(pretty(tree, renderer.dark));
});

Cli

node-treeview

Usage: node-treeview <path> [options]

Options:
  --version, -v   Show version number                                             [boolean]
  --help, -h      Show help                                                       [boolean]
  --all, -a       Include hidden files in output                                  [boolean]
  --content, -c   Add files content to output                                     [boolean]
  --depth, -d     Maximum depth of directories                       [number] [default: -1]
  --relative, -r  Use relative path                                               [boolean]
  --include, -i   List of directory paths to include in output                      [array]
  --exclude, -e   List of directory paths to exclude from output                    [array]
  --glob, -g      Match files based on glob pattern                                 [array]
  --sort, -s      Sort output 0 (Alpha), 1 (FileFirst), 2 (DirFirst)  [number] [default: 0]
  --clean, -n     Clean empty directories from output                             [boolean]
  --flatten, -f   Flatten output                                                  [boolean]
  --pretty, -p    Pretty-print output                                              [string]
  --watch, -w     Watch filesystem                                                [boolean]
  --output, -o    Output file path                                                 [string]
  --debug         Add debugging information to output                             [boolean]

Contribute

git clone https://github.com/avine/node-treeview.git
cd ./node-treeview
npm install
npm run all # npm run clean && npm run build && npm test

License

MIT @ Avine