Electron Adventures: Episode 13: Svelte
Tags: electron, javascript, svelte
Published: 2021-08-05 22:49:51

Bundlers are a curse upon Javascript ecosystem. In just about every other language, to write an app you just create some files with code in that language, and do whatever's equivalent of `npm install some-packages` and things just work.

For some crazy reason in Javascript every project needs a bundler like rollup, webpack, or snowpack, and a bunch of convoluted config files nobody understands, so people just copy them from some boilerplate repo, and hope for the best.

Bundlers went through a lot of iterations, and unfortunately the much promised simplicity never arrived. Configuring them from scratch is just as much pain as it always has been.

I'm starting with Svelte today, but we'll go through very similar process with pretty much any other frontend framework.

### How to use bundlers with Electron

To create an app, the correct order is to setup frontend project with whichever bundler you need first, from your favorite boilerplate. Then clean up the stuff you don't need. Then add Electron to it as the last step.

Don't try to create Electron app, then add a bundler to it. This order has much higher risk that you'll end up wasting precious hours of your life on editing bundlers' stupid config files. Did I mention I hate those config files already? Because I absolutely do.

### Create a new Svelte app from boilerplate

So we first create a Svelte app with `degit`, but we'll be stripping out most of it. We definitely need `rollup.config.js` and `package.json` as that's what we got the boilerplate for. We can keep `package-lock.json` and `.gitignore` as well. Then just delete everything else, it will only get in a way:

```shell
$ npx degit sveltejs/template episode-13-svelte
$ cd episode-13-svelte
$ rm -rf scripts public src README.md
```

### Add Electron

No special steps needed here:

```shell
$ npm i --save-dev electron
```

### Bundler modes

Different bundlers have basically 3 modes:

* a command that compiles the whole thing and outputs static files - this is what we usually do for production builds; in our case `npm run build` will do this. We'll get there once we get to the subject of packaging Electron apps, but not yet.
* often there's some command that watches for changes in the source code, and recompiles the app whenever the source changes. Our boilerplate doesn't use any of that, but rollup can be configured for this with `npx rollup -w`.
* a dev web server mode that serves compiled files without saving them to intermediate places. `npm run dev` does that (not to be confused with `npm run start`)

So unlike in all previous episodes, we'll need to open two terminals:

* one with `npm run dev` - which you can keep running in background; you don't normally need to restart this
* second with `npx electron .` - which you can restart whenever you want to restart the app

For frontend-only changes you can just reload the page, and it will just work. For backend changes you'll need to restart `npx electron .` command too.

### Add backend script `index.js`

We can take existing file, just point it at our dev server. When we package the app we'll need to make it aware of which environment it's in, and to point at that URL, or at the generated file, based on that.

```javascript
let { app, BrowserWindow } = require("electron")

function createWindow() {
  let win = new BrowserWindow({
    webPreferences: {
      preload: `${__dirname}/preload.js`,
    },
  })
  win.maximize()
  win.loadURL("http://localhost:5000/")
}

app.on("ready", createWindow)

app.on("window-all-closed", () => {
  app.quit()
})
```

### Add preload script `preload.js`

We don't need to do any changes, so taking it directly from the previous episode:

```javascript
let child_process = require("child_process")
let { contextBridge } = require("electron")

let runCommand = (command) => {
  return child_process.execSync(command).toString().trim()
}

contextBridge.exposeInMainWorld(
  "api", { runCommand }
)
```

### Add main page `public/index.html`

We need to point at `bundle.js` and `bundle.css` both coming from the `rollup` bundler. Doing it this way makes it work in both development mode, and when application in properly packaged:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Episode 13 - Svelte</title>
  </head>
  <body>
    <link rel="stylesheet" href="/build/bundle.css">
    <script src="/build/bundle.js"></script>
  </body>
</html>
```

### Add Svelte start script `src/main.js`

This script imports the app, and attaches it to the page. There's one that's part of the boilerplate, but it's honestly way too complicated, so here's a simpler version:

```javascript
import App from './App.svelte'
let app = new App({target: document.body})
export default app
```

### Add Svelte app `src/App.svelte`

It's the same terminal app, split into main component, and two other components - one for history entry, and another for command input. If you know Svelte it should be very clear what's going on.

When the form submits, we run `window.api.runCommand`, which we created in `preload`. Unfortunately as this command is synchronous, it's possible to hang up your Svelte app. We'll deal with it later.

The `history.push(newEntry); history=history` is a way to tell Svelte that `history` just got modified.

```svelte
<script>
  import HistoryEntry from "./HistoryEntry.svelte"
  import CommandInput from "./CommandInput.svelte"

  let history = []

  function onsubmit(command) {
    let output = window.api.runCommand(command)
    history.push({command, output})
    history = history
  }
</script>

<h1>Svelte Terminal App</h1>

<div id="terminal">
  <div id="history">
    {#each history as entry}
      <HistoryEntry {...entry} />
    {/each}
  </div>

  <CommandInput {onsubmit} />
</div>

<style>
:global(body) {
  background-color: #444;
  color: #fff;
  font-family: monospace;
}
</style>
```

### Add Svelte component `src/HistoryEntry.svelte`

It's mostly same as previous episode, I simplified CSS a bit, with `gap`. This component is only responsible for display, and doesn't have any logic.

```svelte
<script>
  export let command, output
</script>

<div class='input-line'>
  <span class='prompt'>$</span>
  <span class='input'>{command}</span>
</div>
<div class='output'>{output}</div>

<style>
  .output {
    color: #afa;
    white-space: pre;
    padding-bottom: 0.5rem;
  }

  .input-line {
    display: flex;
    gap: 0.5rem;
  }

  .input {
    color: #ffa;
    flex: 1;
  }
</style>
```

### Add Svelte component `src/CommandInput.svelte`

This component calls back the main application whenever the user submits a command, and then clears it.

I also simplified the CSS a bit compared with previous episodes, with `gap` and `*: inherit`.

```svelte
<script>
  export let onsubmit
  let command = ""

  function submit() {
    onsubmit(command)
    command = ""
  }
</script>

<div class="input-line">
  <span class="prompt">$</span>
  <form on:submit|preventDefault={submit}>
    <input type="text" autofocus bind:value={command} />
  </form>
</div>

<style>
  .input-line {
    display: flex;
    gap: 0.5rem;
  }

  form {
    flex: 1;
    display: flex;
  }

  input {
    flex: 1;
    font-family: inherit;
    background-color: inherit;
    color: inherit;
    border: none;
  }
</style>
```

### Result

And here's the result:

![Episode 13 screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y020hc62rdfiwla4p9d6.png)

That was a long one, and I pretty much assumed you understand some basic Svelte, and just want to show how it works with Electron. If you need a Svelte tutorial, [there's a great one on Svelte website](https://svelte.dev/tutorial/basics). If you want to keep following my Electron Adventures along, it's probably a good idea to get some basics, as that's what I plan to be using the most.

But it won't be an exclusively Svelte series, and in fact in the next episode we'll try to do the same with React and webpack.

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