DEV Community

loading...

Electron Adventures: Episode 13: Svelte

Tomasz Wegrzanowski
Updated on ・5 min read

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:

$ npx degit sveltejs/template episode-13-svelte
$ cd episode-13-svelte
$ rm -rf scripts public src README.md
Enter fullscreen mode Exit fullscreen mode

Add Electron

No special steps needed here:

$ npm i --save-dev electron
Enter fullscreen mode Exit fullscreen mode

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.

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()
})
Enter fullscreen mode Exit fullscreen mode

Add preload script preload.js

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

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

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

contextBridge.exposeInMainWorld(
  "api", { runCommand }
)
Enter fullscreen mode Exit fullscreen mode

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:

<!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>
Enter fullscreen mode Exit fullscreen mode

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:

import App from './App.svelte'
let app = new App({target: document.body})
export default app
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

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.

<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>
Enter fullscreen mode Exit fullscreen mode

Result

And here's the result:

Episode 13 screenshot

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. 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.

Discussion (0)