A file-system browser component for Electron/Vue.js applications

By: (plus.google.com) +David Herron; Date: July 30, 2018

Tags: Node.JS » Electron » Vue.js

Many kinds of applications need to browse files in the file system. For example most programmers text editors (I'm writing this in Microsoft Visual Studio Code) include a sidebar showing the file-system. Therefore it's important to have such a component to use in Electron/Vue.js applications. In this article we'll look at one way to implement such a component.

To follow along with this article, clone the following repositories:

Let's initialize the application:

$ vue init simulatedgreg/electron-vue electron-vue-file-browse

? Application Name electron-vue-file-browse
? Project description Electron/Vue.js file browser
? Select which Vue plugins to install vue-electron, vue-router
? Use linting with ESLint? No
? Set up unit testing with Karma + Mocha? No
? Set up end-to-end testing with Spectron + Mocha? No
? What build tool would you like to use? builder
? author David Herron <david@davidherron.com>

   vue-cli · Generated "electron-vue-file-browse".

---

All set. Welcome to your new electron-vue project!

Make sure to check out the documentation for this boilerplate at
https://simulatedgreg.gitbooks.io/electron-vue/content/.

Next Steps:

  $ cd electron-vue-file-browse
  $ yarn (or `npm install`)
  $ yarn run dev (or `npm run dev`)

The workspace at simulatedgreg/electron-vue includes a very useful framework for developing Electron applications using Vue.js. This downloads the skeleton anc creates the named directory. I've configured it to use vue-electron and vue-router but not the other components, and to not include any ESLint or unit testing features. You may find it useful to answer these questions differently. The framework includes support for both electron-builder and electron-packager and I've chosen the former.

Next we set up dependencies:

$ cd electron-vue-file-browse
$ npm install buefy walkdir path-directories vue-awesome sl-vue-tree --save
$ npm install
$ npm run dev

The last step is somewhat not-useful at the moment because we've done no configuration.

The chosen dependencies include:

  • Buefy is the Vue.js package that uses Bulma for components
  • Walkdir supports tracking changes in the files under a given directory
  • path-directories helps with splitting directory names that will be useful in building the file browser object tree
  • vue-awesome encapsulates the Font Awesome icons as Vue.js components
  • sl-vue-tree is a nice tree view component. We'll use this to store the file system contents

Let's start the surgery on the skeleton application.

src/main/main.js - the Main modeule in the Main process

We'll make a few select changes in main.js to tweak how the application launches.

mport { 
  app, BrowserWindow, ipcMain, dialog 
} from 'electron';
import path from 'path';
import walkdir from 'walkdir';

Change the imports as shown.

In the end of the supplied createWindow function add this line of code:

scanDirectory();

Then add the implementation of this function:

const rootdir = '/Volumes/Extra/akasha-tools/electron-vue-file-browse';

function scanDirectory() {
  walkdir('src', {})
  .on('file', (fn, stat) => {
    mainWindow.webContents.send('file', fn.slice(rootdir.length + 1), stat);
  })
  .on('directory', (fn, stat) => {
    mainWindow.webContents.send('directory', fn.slice(rootdir.length + 1), stat);
  })
  .on('error', (fn, err) => {
    console.error(`!!!! ${fn} ${err}`);
  });
}

The idea is to "walk" a selected directory tree, sending out information about the files in that tree. Since this is a test application there are some hard-coded values. For a real application we'd have a file browser dialog to select a directory.

The walkdir module ( (www.npmjs.com) https://www.npmjs.com/package/walkdir) "Walks a directory tree emitting events based on what it finds." As it traverses the directory tree it emits events concerning what it finds. We simply forward those events to the Renderer process.

The fn argument is the complete pathname that is found. On studying this application it seems more useful to send not the complete pathname, but the segment following the root directory being scanned. Therefore we use the slice function to trim the string appropriately.

ipcMain.on('rescan-directory', () => {
  scanDirectory();
});

This message can be sent from the Renderer process to cause the Main process to rescan the directory structure.

process.on('unhandledRejection', (reason, p) => {
  console.error(`Unhandled Rejection at: ${util.inspect(p)} reason: ${reason}`);
});

This catches any unhandle promise rejection errors. See: Handling unhandled Promise rejections, avoiding application crash

src/renderer/main.js - the Main module in the Renderer process

Change main.js to the following

import Vue from 'vue'

import { ipcRenderer } from 'electron';

import App from './App';
import router from './router';
import Buefy from 'buefy';
import 'buefy/lib/buefy.css';

Vue.use(Buefy);

if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.config.productionTip = false

export const messageBus = new Vue({
});

/* eslint-disable no-new */
new Vue({
  components: { App },
  router,
  template: '<App/>'
}).$mount('#app')


ipcRenderer.on('file', (event, fn, stat) => {
  messageBus.$emit('file', fn, stat);
});

ipcRenderer.on('directory', (event, fn, stat) => {
  messageBus.$emit('directory', fn, stat);
});

Some of this carries over from the skeleton, and the rest initializes support for some of the dependencies we loaded, as well as useful objects.

We initialize Buefy (the UI toolkit), sl-tree-vue (the tree viewer), and vue-awesome (Font Awesome).

The messageBus object is an extra Vue instance we'll use for sending messages around the application. Vue.js has great support for a child component to send messages to its parent component. But out of the box it does not directly support sending messages between any two components. We will use messageBus to do so - since all Vue instances contain capabilities to emit and listen to messages.

We see two such messages - the file and directory events are received from the Main process, and then are emitted on the messageBus.

src/renderer/router/index.js -- The Router

For this application the Router is not really required, but I said "yes" to it above so the code is here. This module is where you would handle multiple application pages. As it stands there is a single route for the / URL, and it refers to a LandingPage component. Change it to this:

export default new Router({
  routes: [
    {
      path: '/',
      name: 'file-browser-page',
      component: require('@/components/FileBrowserMain').default
    },
    {
      path: '*',
      redirect: '/'
    }
  ]
});

src/renderer/components/FileBrowserMain.vue

What we've done so far is position this component as the main window of the application. We'll implement it as a single-file-template that will in turn demonstrate using a file-system oriented tree browser.

In src/renderer/components delete all the supplied files. Those files comprise the demo application, and we need none of that code.

Create FileBrowserTree.vue to contain this:

<template>
  <div id="wrapper" class="columns is-gapless is-mobile">
    <file-browser-tree 
          id="file-tree"
          ref="filetree"
          class="column"
          @nodeClick="nodeClick"
          @nodeDoubleClick="nodeDoubleClick"
          @nodeDrop="nodeDrop">

    <template slot="context-menu">
        <div @click="doDashboard">Dashboard</div>
        <div @click="doCustomers">Customers</div>
    </template>

    </file-browser-tree>

    <div id="file-info-view" class="column">
        <span v-html="fileInfo"></span>
        <button class="button is-primary" @click="rescan">Rescan</button>
    </div>
  </div>
</template>

<script>

const path = require('path');
const util = require('util');
import { messageBus } from '../main.js';
import { ipcRenderer } from 'electron';

import FileBrowserTree from 'vue-file-tree';

export default {
  name: 'file-browser-main',
  components: {
    'file-browser-tree': FileBrowserTree,
  },
  data() {
    return {
      fileInfo: "",
      nodes: []
    }
  },
  methods: {
    nodeClick(event, node) {
      this.fileInfo = `
        <table>
        <tr><th>ACTION</th><th>CLICK</th></tr>
        <tr><th>Filename</th><td>${node.data.pathname}</td></tr>
        <tr><th>Created</th><td>${node.data.stat.ctime}</td></tr>
        <tr><th>Access</th><td>${node.data.stat.atime}</td></tr>
        <tr><th>Modified</th><td>${node.data.stat.mtime}</td></tr>
        <tr><th>Size</th><td>${node.data.stat.size}</td></tr>
        <tr><th>Mode</th><td>${node.data.stat.mode}</td></tr>
        </table>
        `;
    },
    nodeDoubleClick(node) {
      this.fileInfo = `
        <table>
        <tr><th>ACTION</th><th>DOUBLE-CLICK</th></tr>
        <tr><th>Filename</th><td>${node.data.pathname}</td></tr>
        <tr><th>Created</th><td>${node.data.stat.ctime}</td></tr>
        <tr><th>Access</th><td>${node.data.stat.atime}</td></tr>
        <tr><th>Modified</th><td>${node.data.stat.mtime}</td></tr>
        <tr><th>Size</th><td>${node.data.stat.size}</td></tr>
        <tr><th>Mode</th><td>${node.data.stat.mode}</td></tr>
        </table>
        `;
    },
    nodeDrop(node) {
      this.fileInfo = `
        <table>
        <tr><th>ACTION</th><th>DROP</th></tr>
        <tr><th>Filename</th><td>${node[0].data.pathname}</td></tr>
        <tr><th>Created</th><td>${node[0].data.stat.ctime}</td></tr>
        <tr><th>Access</th><td>${node[0].data.stat.atime}</td></tr>
        <tr><th>Modified</th><td>${node[0].data.stat.mtime}</td></tr>
        <tr><th>Size</th><td>${node[0].data.stat.size}</td></tr>
        <tr><th>Mode</th><td>${node[0].data.stat.mode}</td></tr>
        </table>
        `;
    },
    doCustomers() {
        console.log(`doCustomers`);
        this.$refs.filetree.contextMenuIsVisible = false;
    },
    doDashboard() {
        console.log(`doDashboard`);
        this.$refs.filetree.contextMenuIsVisible = false;
    },
    rescan() {
      this.nodes = [];
      ipcRenderer.send('rescan-directory');
    }
  },
  created: function() {
    console.log(util.inspect(path));
      messageBus.$on('file', (fn, stat) => { 
        this.$refs.filetree.addPathToTree(fn, stat, false);
      });
      messageBus.$on('directory', (fn, stat) => { 
        this.$refs.filetree.addPathToTree(fn, stat, true);
      });
  }
}
</script>

<style>
  @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro');

  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  body { font-family: 'Source Sans Pro', sans-serif; }

  #wrapper {
    height: 100vh;
    width: 100vw;
  }

  #file-tree, #file-info-view {
    height: 100%;
    overflow: scroll;
  }

</style>

In the <template> we have a simple Buefy columns component. In the left-side column is the file browser, and the right side is an area to display information. The right side also includes a button to cause rescanning of the directory content.

The Rescan button causes a message to be sent to the Main process. If you refer back to the Main process you see this message causes the chosen directory to be scanned.

When the chosen directory is scanned a series of messages are sent here from the Main process. Those messages are either file or directory events, and contain both a pathname and the stat object corresponding to the file. Those messages are handled in event handlers, and turn into addPathToTree calls which add the pathname to the tree object. We'll get to that a little later.

Note: <file-browser-tree ... ref="filetree" ...></file-browser-tree>

The ref= attribute allows us to reference the component's object as so: this.$refs.filetree.addPathToTree(fn, stat, false);

Vue.js makes sure the $refs object has fields corresponding to every component with a ref= attribute. This component is documented a little further into the article.

We have three handlers for events emitted by that component:

  • nodeClick Handles clicks on items
  • nodeDoubleClick Handles double-clicks on items
  • nodeDrop Handles drag-and-drop operations on items

In this demo application each prints information in the information pane.

The file tree component also supports a context menu, activated by right-click. We use <template slot-name="context-menu"> to inject menu choices into the menu. It automatically pops up at the location of the mouse pointer. The context menu items in the demo application are simply examples, each of which invokes a handler method. An important step of these handler methods is this:

this.$refs.filetree.contextMenuIsVisible = false;

This line of code instructs the file tree component to dismiss the menu.

vue-file-tree component

We've mentioned this component several times, and now it is time to investigate it.

The repository for the component has a whole build configuration and other scripts. See (github.com) https://github.com/robogeek/vue-file-tree for details. In this article we'll only investigate the .vue file.

To use this component in your application add the following to the dependencies section of the package.json in your application: "vue-file-tree": "github:robogeek/vue-file-tree",. Then follow the further directions in the README, or work it out from the code presented earlier.

The template for this component:

<template>
    <span>
    <sl-vue-tree
            id="vue-file-tree"
            ref="slvuetree"
            :value="nodes"
            :allowMultiselect="false"
            @nodeclick="nodeClick"
            @nodedblclick="nodeDoubleClick"
            @select="nodeSelect"
            @toggle="nodeToggle"
            @drop="nodeDrop"
            @nodecontextmenu="nodeContextMenu"
            @externaldrop.prevent="onExternalDropHandler">

        <template slot="toggle" slot-scope="{ node }">
            <span v-if="!node.isLeaf">
                <font-awesome-icon 
                    icon="caret-right" 
                    v-if="!node.isExpanded"></font-awesome-icon>
                <font-awesome-icon 
                    icon="caret-down"
                    v-else-if="node.isExpanded"></font-awesome-icon>
            </span>
        </template>

        <template slot="title" slot-scope="{ node }">
            <font-awesome-icon 
                :icon="[ 'fab', 'js' ]" 
                v-if='node.data.type === "application/javascript"'></font-awesome-icon>
            <font-awesome-icon 
                icon="table" 
                v-else-if='node.data.type === "application/json"'></font-awesome-icon>
            <font-awesome-icon 
                icon="image" 
                v-else-if='node.data.type === "IMAGE"'></font-awesome-icon>
            <font-awesome-icon 
                icon="code" 
                v-else-if='node.data.type === "EJS"'></font-awesome-icon>
            <font-awesome-icon 
                :icon="[ 'fab', 'vuejs' ]" 
                v-else-if='node.data.type === "VUEJS"'></font-awesome-icon>
            <font-awesome-icon 
                icon="file" 
                v-else-if="node.isLeaf"></font-awesome-icon>
            {{ node.title }} </template>


        <template slot="sidebar" slot-scope="{ node }">
            <font-awesome-icon 
                icon="circle" 
                v-if="node.data.isModified"></font-awesome-icon>
        </template>
    </sl-vue-tree>


    <aside class="menu vue-file-tree-contextmenu" 
            ref="contextmenu" 
            v-show="contextMenuIsVisible">
        <slot name="context-menu"></slot>
    </aside>

    </span>
</template>

What we're doing is to implement a wrapper around yet another component, whose repository is: (github.com) https://github.com/holiber/sl-vue-tree

There are a number of event handlers and other configuration we've implemented to configure default behavior that makes sense as a tree component displaying files in the file system.

The sl-vue-tree component supports a number of slot's into which we can inject HTML. We've added these:

  • toggle This determines which arrow button to show for directory nodes which are expanded, versus ones which are closed.
  • title This describes one row of the tree component. We want to show an icon based on the file type. We have a means to select these codes to select the correct icon.
  • sidebar This describes an icon that appears at the far-right of the tree display.

We also have a slot of our own, context-menu, which is where the context menu HTML arrives. In this case we are using a Buefy menu component. The display of this component is controlled by the contextMenuIsVisible data item.

Obviously we are using the Font Awesome icon set.

Next, we add the <script> tag:

<script>

import path from 'path';
import util from 'util';
import splitter from './path-splitdirs';

import mime from 'mime';

import slVueTree from 'sl-vue-tree';
// import 'sl-vue-tree/dist/sl-vue-tree-dark.css';

import { library } from '@fortawesome/fontawesome-svg-core';
import {
  faCaretRight, faCaretDown, faTable, faImage, faFile, faCircle, faCode
} from '@fortawesome/free-solid-svg-icons';
import { faJs, faVuejs } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

library.add(faJs, faVuejs, faCaretRight, faCaretDown, faTable, faImage, faFile, faCircle, faCode);


/* var nodes = [
    {title: 'Item1', isLeaf: true},
    {title: 'Item2', isLeaf: true, data: { visible: false }},
    {title: 'Folder1'},
    {
      title: 'Folder2', isExpanded: true, children: [
        {title: 'Item3', isLeaf: true},
        {title: 'Item4', isLeaf: true, data: { isModified: true }}
      ]
    }
]; */

export default {

    data() {
        return {
            nodes: [],
            contextMenuIsVisible: false
        }
    },
    components: {
        'sl-vue-tree': slVueTree,
        'font-awesome-icon': FontAwesomeIcon
    },
    created() {
        /*
         * Derived from Buefy's b-dropdown
         * https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
         */
        if (typeof window !== 'undefined') {
            document.addEventListener('click', this.clickedOutside)
        }
    },
    methods: {
        nodeClick(node, event) {
            this.$emit('nodeClick', event, node);
        },
        nodeDoubleClick(node, event) {
            console.log(`nodeDoubleClick ${node.title} ${node.data.type} isLeaf ${node.isLeaf} ${util.inspect(node)}`);
            if (!node.isLeaf) {
                this.$refs.slvuetree.onToggleHandler(event, node);
                return;
            }
            this.$emit('nodeDoubleClick', node);
        },
        nodeSelect(node) {
            console.log(`nodeSelect ${util.inspect(node)}`);
        },
        nodeToggle(node) {
            console.log(`nodeToggle ${util.inspect(node)}`);
        },
        nodeDrop(node) {
            console.log(`nodeDrop ${util.inspect(node)}`);
            this.$emit('nodeDrop', node);
        },
        nodeContextMenu(node, event) {
            console.log(`nodeContextMenu ${util.inspect(node)}`);
            this.contextMenuIsVisible = true;
            const $contextMenu = this.$refs.contextmenu;
            $contextMenu.style.left = event.clientX + 'px';
            $contextMenu.style.top = event.clientY + 'px';
        },
        /**
         * Close dropdown if clicked outside.
         * Derived from Buefy's b-dropdown
         * https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
         */
        clickedOutside(event) {
            if (!this.isInWhiteList(event.target)) this.contextMenuIsVisible = false;
        },
        // If the "clickOutside" is on a target where we should ignore the click
        // then we should ignore this.  
        // See: https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
        isInWhiteList(el) { return false; },
        onExternalDropHandler(cursorPosition, event) {
            console.log('external drop', cursorPosition, util.inspect(event));
        },
        addPathToTree(fn, stat, isDir) {
            console.log(`addPathToTree ${fn} ${util.inspect(stat)} ${isDir}`);
            console.log(`addPathToTree ${util.inspect(process)}`);
            console.log(util.inspect(path));
            fn = path.normalize(fn);
            console.log(`addPathToTree NORMALIZED ${fn}`);
            const basenm = path.basename(fn);
            console.log(`addPathToTree BASENAME ${basenm}`);

            const split = splitter(fn);

            console.log(`addPathToTree dirs ${util.inspect(split)}`);
            let curnodes = this.nodes;
            for (let dir of split.dirs) {
                if (dir === '.') continue;
                let found = undefined;
                for (let cur of curnodes) {
                    if (cur.isLeaf === false && cur.title === dir) {
                        found = cur;
                        break;
                    }
                }
                if (!found) {
                    let newnode = {
                        title: dir, 
                        isLeaf: false, 
                        children: [], 
                        data: { 
                            type: "DIRECTORY",
                            pathname: fn, 
                            stat 
                        }
                    };
                    console.log(`addPathToTree !found push newnode ${util.inspect(newnode)}`);
                    curnodes.push(newnode);
                    curnodes = newnode.children;
                } else {
                    curnodes = found.children;
                }
            }
            let newnode = {
                title: basenm, 
                isLeaf: !isDir, 
                data: { 
                    type: mime.getType(fn),
                    pathname: fn,
                    stat
                }
            };
            if (!newnode.data.type) newnode.data.type = "text/plain";
            if (newnode.data.type.startsWith('image/')) newnode.data.type = "IMAGE";
            if (fn.endsWith('.ejs')) newnode.data.type = "EJS";
            if (fn.endsWith('.vue')) newnode.data.type = "VUEJS";
            if (!newnode.isLeaf) newnode.children = [];
            console.log(`addPathToTree FINAL push newnode ${util.inspect(newnode)}`);
            curnodes.push(newnode);
        }
    }
}

</script>

At the top of this we import various required modules. This includes the sl-vue-tree component, and some of the Font Awesome icons. Both of those become Vue.js components available to this component.

The sl-vue-tree component expects data items in a particular data structure. When in Bucharest do as the Romanians, so therefore we must give a correct data structure to that component. An example of the data structure is in a comment shown here.

The addPathToTree function receives the file and directory messages described earlier. It receives a simple pathname string plus a Stat object. The task here is to convert that information into the structure required by sl-vue-tree.

I ran into a problem developing this component. The requirement to create that directory structure is to split the pathname string into segments. To do this in a operating system independent manner requires careful considerations.

At first the path-directories module (https://github.com/jy95/path-directories) was used. It takes a pathname and reliably splits it into segments accounting for Windows or POSIX file path conventions. Unfortunately as written it runs on Node.js just fine, but this vue-file-tree component is packaged assuming it is running in a web browser. Where path-directories assumes a path module compatible with Node.js, the packaged Vue.js component receives a path module which presents a subset of Node.js functionality.

Therefore a replacement was coded:

const _path = require('path');
const normalize = require('normalize-path');
const parsePath = require('parse-filepath');

let isWin32 = false;
let isPosix = true;

module.exports = function(path2split) {
    path2split = normalize(path2split);
    if (path2split.match(/^[a-zA-Z]\:/)) { isWin32 = true; isPosix = false; }
    if (path2split.indexOf('\\') >= 0) { isWin32 = true; isPosix = false; }
    let path = isWin32 ? _path.win32 : _path.posix;
    if (!path) path = _path;
    let parsed = path.parse ? path.parse(path2split) : parsePath(path2split);
    if (parsed.root === '') parsed.root = '.';
    let dir = parsed.dir;
    let dirz = [];
    do {
        dirz.unshift(path.basename(dir));
        dir = path.dirname(dir);
    } while (dir !== parsed.root);
    parsed.dirs = dirz;
    return parsed;
}

This module handles pathname normalization and robustly splitting the path irregardless of file system conventions. It returns the object supplied by the path.parse function, augmented with a dirs attribute containing the directory portion of the pathname but split into an array.

What addPathToTree does with this is to add each segment of the file pathname into the data structure required by sl-vue-tree. Then Vue.js arranges for this structure to be handed to sl-vue-tree which causes the file system tree to diaplay.

The other methods handle various events going back and forth.

Finally we get to the <style> section:

<style>
.vue-file-tree-contextmenu {
    position: absolute;
    background-color: white;
    color: black;
    border-radius: 2px;
    cursor: pointer;
}

.vue-file-tree-contextmenu > div {
    padding: 10px;
}

.vue-file-tree-contextmenu > div:hover {
    background-color: rgba(100, 100, 255, 0.5);
}

#vue-file-tree {
    height: 100%;
}

.sl-vue-tree {
    position: relative;
    cursor: default;
    user-select: none;
}

.sl-vue-tree.sl-vue-tree-root {
    border: 1px solid rgb(9, 22, 29);
    background-color: rgb(9, 22, 29);
    color: rgba(255, 255, 255, 0.5);
    border-radius: 3px;
}

.sl-vue-tree-root > .sl-vue-tree-nodes-list {
    overflow: hidden;
    position: relative;
    padding-bottom: 4px;
}

.sl-vue-tree-selected > .sl-vue-tree-node-item {
    background-color: #13242d;
    color: white;
}

.sl-vue-tree-node-item:hover,
.sl-vue-tree-node-item.sl-vue-tree-cursor-hover {
    color: white;
}

.sl-vue-tree-node-item {
    position: relative;
    display: flex;
    flex-direction: row;

    padding-left: 10px;
    padding-right: 10px;
    line-height: 28px;
    border: 1px solid transparent;
}


.sl-vue-tree-node-item.sl-vue-tree-cursor-inside {
    border: 1px solid rgba(255, 255, 255, 0.5);
}

.sl-vue-tree-gap {
    width: 25px;
    min-height: 1px;

}

.sl-vue-tree-toggle {
    display: inline-block;
    text-align: left;
    width: 20px;
}

.sl-vue-tree-sidebar {
    margin-left: auto;
}

.sl-vue-tree-cursor {
    position: absolute;
    border: 1px solid rgba(255, 255, 255, 0.5);
    height: 1px;
    width: 100%;
}

.sl-vue-tree-drag-info {
    position: absolute;
    background-color: rgba(0,0,0,0.5);
    opacity: 0.5;
    margin-left: 20px;
    padding: 5px 10px;
}
</style>

These CSS declarations handle two tasks: a) the context menu, b) styling of the file tree component

Running the application

We now have everything lined up and ready to go. The electron-vue framework makes this easy:

$ npm run dev

> electron-vue-file-browse@0.0.0 dev /Volumes/Extra/akasha-tools/electron-vue-file-browse
> node .electron-vue/dev-runner.js

         ___                      __                                                                
   __   /\_ \       __     ___   /\ \__    _ __    ___     ___              __  __  __  __     __   
 / ,.`\ \//\ \    / ,.`\  /'___\ \ \ ,_\  /\` __\ / __`\ /' _ `\   _______ /\ \/\ \/\ \/\ \  / ,.`\ 
/\  __/   \_\ \_ /\  __/ /\ \__/  \ \ \/  \ \ \/ /\ \_\ \/\ \/\ \ /\______\\ \ \/ |\ \ \_\ \/\  __/ 
\ \____\  /\____\\ \____\\ \____\  \ \ \_  \ \_\ \ \____/\ \_\ \_\\/______/ \ \___/ \ \____/\ \____\
 \/____/  \/____/ \/____/ \/____/   \ \__\  \/_/  \/___/  \/_/\/_/           \/__/   \/___/  \/____/
                                     \/__/                                                          
  getting ready...

┏ Main Process ---------------

  compiling...

┗ ----------------------------

┏ Renderer Process -----------

  Hash: b470657ee609c4cee744
  Version: webpack 3.12.0
  Time: 8084ms
        Asset       Size  Chunks                    Chunk Names
  renderer.js    1.13 MB       0  [emitted]  [big]  renderer
   styles.css     304 kB       0  [emitted]  [big]  renderer
   index.html  508 bytes          [emitted]         
     [0] ./node_modules/vue/dist/vue.esm.js 292 kB {0} [built]
     [1] ./node_modules/vue-hot-reload-api/dist/index.js 6.24 kB {0} [built]
     [5] ./src/renderer/main.js 623 bytes {0} [built]
     [6] external "electron" 42 bytes {0} [not cacheable]
    [12] multi ./.electron-vue/dev-client ./src/renderer/main.js 40 bytes {0} [built]
    [13] ./.electron-vue/dev-client.js 731 bytes {0} [built]
    [14] (webpack)-hot-middleware/client.js?noInfo=true&reload=true 7.77 kB {0} [built]
    [16] external "querystring" 42 bytes {0} [not cacheable]
    [19] (webpack)-hot-middleware/client-overlay.js 2.21 kB {0} [built]
    [25] ./src/renderer/App.vue 1.84 kB {0} [built]
    [29] ./src/renderer/router/index.js 273 bytes {0} [built]
    [37] external "buefy" 42 bytes {0} [not cacheable]
    [38] ./node_modules/buefy/lib/buefy.css 41 bytes {0} [built]
    [39] external "vue-electron" 42 bytes {0} [not cacheable]
    [41] ./node_modules/style-loader/lib/addStyles.js 8.7 kB [built]
      + 28 hidden modules
  Child html-webpack-plugin for "index.html":
           Asset     Size  Chunks  Chunk Names
      index.html  1.45 MB       0  
         [0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.ejs 1.3 kB {0} [built]
         [1] ./node_modules/lodash/lodash.js 540 kB {0} [built]
         [2] (webpack)/buildin/module.js 517 bytes {0} [built]
  Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js!node_modules/buefy/lib/buefy.css:
         [0] ./node_modules/css-loader!./node_modules/buefy/lib/buefy.css 318 kB {0} [built]
         [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]

┗ ----------------------------

The framework supports hot reload while the app is running in dev mode.

While scanning the directory tree these messages are printed:

┏ Electron -------------------

  walkdir sending FILE /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/index.ejs
  walkdir sending DIR /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/main
  walkdir sending DIR /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/renderer
  
┗ ----------------------------

┏ Electron -------------------

  walkdir sending FILE /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/main/index.dev.js
  
┗ ----------------------------

This is what the application looks like during execution.

« Fix Webpack's ability to load CSS while packaging an app With a Node.js based REST service, is it okay to not use a front-end framework like React or Vue.js? »
2016 Election Acer C720 Ad block AkashaCMS Amazon Amazon Kindle Amazon Web Services America Amiga and Jon Pertwee Android Anti-Fascism AntiVirus Software Apple Apple Hardware History Apple iPhone Apple iPhone Hardware April 1st Arduino ARM Compilation Artificial Intelligence Astronomy Astrophotography Asynchronous Programming Authoritarianism Automated Social Posting AWS DynamoDB AWS Lambda Ayo.JS Bells Law Big Brother Big Finish Bitcoin Mining Black Holes Blade Runner Blockchain Blogger Blogging Books Botnets Cassette Tapes Cellphones China China Manufacturing Christopher Eccleston Chrome Chrome Apps Chromebook Chromebox ChromeOS CIA CitiCards Citizen Journalism Civil Liberties Clinton Cluster Computing Command Line Tools Comment Systems Computer Accessories Computer Hardware Computer Repair Computers Cross Compilation Crouton Cryptocurrency Curiosity Rover Currencies Cyber Security Cybermen Daleks Darth Vader Data backup Data Storage Database Database Backup Databases David Tenant DDoS Botnet Detect Adblocker Developers Editors Digital Photography Diskless Booting Disqus DIY DIY Repair DNP3 Do it yourself Docker Docker MAMP Docker Swarm Doctor Who Doctor Who Paradox Doctor Who Review Drobo Drupal Drupal Themes DVD E-Books E-Readers Early Computers Election Hacks Electric Bicycles Electric Vehicles Electron Emdebian Encabulators Energy Efficiency Enterprise Node EPUB ESP8266 Ethical Curation Eurovision Event Driven Asynchronous Express Face Recognition Facebook Fake News Fedora VirtualBox File transfer without iTunes FireFly Flickr Fraud Freedom of Speech Front-end Development Gallifrey git Github GitKraken Gitlab GMAIL Google Google Chrome Google Gnome Google+ Government Spying Great Britain Heat Loss Hibernate Hoax Science Home Automation HTTP Security HTTPS Human ID I2C Protocol Image Analysis Image Conversion Image Processing ImageMagick In-memory Computing InfluxDB Infrared Thermometers Insulation Internet Internet Advertising Internet Law Internet of Things Internet Policy Internet Privacy iOS Devices iPad iPhone iPhone hacking Iron Man iTunes Java JavaScript JavaScript Injection JDBC John Simms Journalism Joyent Kaspersky Labs Kindle Kindle Marketplace Lets Encrypt LibreOffice Linux Linux Hints Linux Single Board Computers Logging Mac Mini Mac OS Mac OS X Machine Learning Machine Readable ID macOS MacOS X setup Make Money Online March For Our Lives MariaDB Mars Mass Violence Matt Lucas MEADS Anti-Missile Mercurial MERN Stack Michele Gomez Micro Apartments Microsoft Military AI Military Hardware Minification Minimized CSS Minimized HTML Minimized JavaScript Missy Mobile Applications Mobile Computers MODBUS Mondas Monetary System MongoDB Mongoose Monty Python MQTT Music Player Music Streaming MySQL NanoPi Nardole NASA Net Neutrality Network Attached Storage Node Web Development Node.js Node.js Database Node.js Testing Node.JS Web Development Node.x North Korea npm NVIDIA NY Times Online advertising Online Community Online Fraud Online Journalism Online Photography Online Video Open Media Vault Open Source Open Source Governance Open Source Licenses Open Source Software OpenAPI OpenVPN Palmtop PDA Patrick Troughton Paywalls Personal Flight Peter Capaldi Phishing Photography PHP Plex Plex Media Server Political Protest Postal Service Power Control Privacy Production use Public Violence Raspberry Pi Raspberry Pi 3 Raspberry Pi Zero ReactJS Recaptcha Recycling Refurbished Computers Remote Desktop Removable Storage Republicans Retro Computing Retro-Technology Reviews RFID Right to Repair River Song Robotics Rocket Ships RSS News Readers rsync Russia Russia Troll Factory Russian Hacking Rust SCADA Scheme Science Fiction SD Cards Search Engine Ranking Season 1 Season 10 Season 11 Security Security Cameras Server-side JavaScript Serverless Framework Servers Shell Scripts Silence Simsimi Skype SmugMug Social Media Social Media Warfare Social Network Management Social Networks Software Development Space Flight Space Ship Reuse Space Ships SpaceX Spear Phishing Spring Spring Boot Spy Satellites SQLite3 SSD Drives SSD upgrade SSH SSH Key SSL Stand For Truth Strange Parts Swagger Synchronizing Files Telescopes Terrorism The Cybermen The Daleks The Master Time-Series Database Tom Baker Torchwood Total Information Awareness Trump Trump Administration Trump Campaign Twitter Ubuntu Udemy UDOO US Department of Defense Virtual Private Networks VirtualBox VLC VNC VOIP Vue.js Web Applications Web Developer Resources Web Development Web Development Tools Web Marketing Webpack Website Advertising Weeping Angels WhatsApp William Hartnell Window Insulation Windows Windows Alternatives Wordpress World Wide Web Yahoo YouTube YouTube Monetization