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
Add Electron
No special steps needed here:
$ 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 withnpm 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()
})
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 }
)
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>
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
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>
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>
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>
Result
And here's the result:
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.
Top comments (1)
It's not crazy at all... it looks like you are not fully familiar with the problem space.
It's because browsers have to download the code, on a huge array of devices with different support, working with a single thread that shouldn't be blocked, not only running the app, but also rendering the page. All all within milliseconds, or the user gets bored and leaves. Oh, and you must support design systems because corporate designers need to enforce their visual guidelines on every aspect of the interface.
It's incredibly complex stuff, much more than everything you have to deal with in the back end