Electron Adventures: Episode 48: path-browserify
Tags: javascript, svelte, electron
Published: 2021-09-11 03:29:48

As I was adding dialogs to the file manager, I noticed that a lot of that new functionality will require path manipulation. And it's already the most messy part of the code.

Path manipulation isn't difficult, so it's tempting to just do some regexp in various places, but it adds up to make code unclear. This is espeecially so since Javascript lacks such simple operations as "get last element of array".

So in Ruby it's possible to do:

```ruby
filepath.split("/").last
```

JavaScript requires nasty code like:

```javascript
filepath.split("/").slice(-1)[0]
```

Interestingly at least [this one is soon coming to Javascript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at), and it will soon be possible to write code like this:

```javascript
filepath.split("/").at(-1)
```

### `path-browserify`

Backend JavaScript has `path` module which handles common path manipulation, but browser APIs have nothing like it.

Fortunately `path` is basically a bunch of regular expresions that don't depend on backend functionality in any way.

To get access to it from the browser we just need to install it:

```shell
$ npm install path-browserify
```

For Electron we could also expose it from the preload, but this a very poor practice. If something can be done on purely frontend side just fine, it's better to do it frontend side, as preload is security-sensitive code.

### `src/Panel.svelte`

First we need to import `path`:

```javascript
import path from "path-browserify"
```

The template used to have `<header>{directory.split("/").slice(-1)[0]}</header>`. We don't want code like that. Instead let's extract that to `header`


```html
<div class="panel {id}" class:active={active}>
  <header>{header}</header>
  <div class="file-list" bind:this={fileListNode}>
    {#each files as file, idx}
      <File
        panelId={id}
        file={file}
        idx={idx}
        focused={idx === focusedIdx}
        selected={selected.includes(idx)}
        bind:node={fileNodes[idx]}
      />
    {/each}
  </div>
</div>
```

The header is now defined using `path.basename` - which replaces former monstrosity. It now also handles `/` correctly. In previous version, it would result in empty header if we got to `/`.

```javascript
  $: header = (directory === "/") ? "/" : path.basename(directory)
```

We can replace path manipulation in other parts of the code:

```javascript
  $: focusedPath = focused && path.join(directory, focused.name)

  function activateItem() {
    if (focused?.type === "directory") {
      if (focused.name === "..") {
        initialFocus = path.basename(directory)
      } else {
        initialFocus = null
      }
      directory = path.join(directory, focused.name)
    }
  }
```

That just leaves two checks we do manually, and honestly they're perfectly readable as is without any helper functions:

* is it `..` - by `focused?.name === ".."`
* is it `/` - by `directory === "/"`

### `src/App.svelte`

We start by importing `path`:

```javascript
  import path from "path-browserify"
```

There are two places where we use it. First when we start we do this to set the initial directory:

```javascript
  let initialDirectoryLeft = window.api.currentDirectory()
  let initialDirectoryRight = path.join(window.api.currentDirectory(), "node_modules")
```

To be honest we should probably save it in local storage or something, but it will do.

And next we cas use `path.extname` to get extension of the file:

```javascript
  function viewFile(file) {
    let ext = path.extname(file).toLowerCase()
    if (ext === ".png") {
      preview = {type: "image", file, mimeType: "image/png"}
    } else if (ext === ".jpg" || ext === ".jpeg") {
      preview = {type: "image", file, mimeType: "image/jpeg"}
    } else if (ext === ".gif") {
      preview = {type: "image", file, mimeType: "image/gif"}
    } else if (/\.(css|js|json|md|txt|svelte)$/i.test(ext)) {
      preview = {type: "text", file}
    } else {
      window.api.viewFile(file)
    }
  }
```

This lets us replace some regexps by `===`, but for longer lists, regexp is still much more concise.

And finally we need to replace various variables called `path` by something else like `file`, as `import path` would conflict with it.

This is a problem most other languages don't have - for Ruby uses uppercase names like `Pathname` or `URL` for modules, and lowercase names like `path` or `url` for local variables. And for that matter makes them into proper objects of appropriate types, so in Ruby version we'd be doing `file.extname` and `directory + "node_modules"` not `path.extname(file)` and `path.join(directory, "node_modules")`, and it would do the right thing.

These are small issues, but they add up to JavaScript being a poor language. Unfortunately we're pretty much stuck with it for user interfaces for the time being.

### Result

Here's the results:

![Episode 48 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b85uze01jlqrj08ck4uk.png)

In the next episode, we'll take another go at adding dialogs to the app.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-48-path-browserify).