Let's continue exploring Electron alternatives. This time, Neutralino.
As we've done the terminal apps so many times already, and the changes are minimal, I won't do separate hello world episode, followed by terminal app episode. I'll just implement terminal app as before. As it won't be all that different from what we had before, I won't be going through everything in too much detail.
Installation
I tried to create a new app with npx
but it didn't work
$ npm install -g @neutralinojs/neu
$ neu create episode-74-neutralino
After cleaning up some code we don't need, I went on to implement the usual terminal app.
neutralino.config.json
Here's the neutralino.config.json
file created by the installer:
{
"applicationId": "js.neutralino.sample",
"port": 0,
"defaultMode": "window",
"enableHTTPServer": true,
"enableNativeAPI": true,
"url": "/resources/",
"nativeBlockList": [],
"globalVariables": {
"TEST": "Test Value"
},
"modes": {
"window": {
"title": "episode-74-neutralino",
"width": 800,
"height": 500,
"minWidth": 400,
"minHeight": 250,
"fullScreen": false,
"alwaysOnTop": false,
"enableInspector": true,
"borderless": false,
"maximize": false
},
"browser": {},
"cloud": {}
},
"cli": {
"binaryName": "episode-74-neutralino",
"resourcesPath": "/resources/",
"clientLibrary": "/resources/js/neutralino.js",
"binaryVersion": "2.8.0",
"clientVersion": "1.5.0"
}
}
We specify entry point, window size and so on. This information is in package.json
in NW.js, and in index.js
in Electron.
resources/index.html
The root page has some extra js/neutralino.js
stuff, other than that it just loads our CSS and code, and has some placeholders for our app to work with.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Neutralino terminal app</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Neutralino terminal app</h1>
<div id="terminal">
<div id="history">
</div>
<div class="input-line">
<span class="prompt">$</span>
<form>
<input type="text" autofocus />
</form>
</div>
</div>
<!-- Neutralino.js client. This file is gitignored,
because `neu update` typically downloads it.
Avoid copy-pasting it.
-->
<script src="js/neutralino.js"></script>
<!-- Your app's source files -->
<script src="js/main.js"></script>
</body>
</html>
resources/styles.css
This is identical to what we had before - simple dark mode terminal app.
body {
margin: 1em;
background-color: #333;
color: #fff;
font-family: monospace;
}
header {
text-align: center;
font-size: 400%;
font-family: monospace;
}
.input-line {
display: flex;
}
.input-line > * {
flex: 1;
}
.input-line > .prompt {
flex: 0;
padding-right: 0.5rem;
}
.output {
padding-bottom: 0.5rem;
}
.input {
color: #ffa;
}
.output {
color: #afa;
white-space: pre;
}
form {
display: flex;
}
input {
flex: 1;
font-family: inherit;
background-color: #444;
color: #fff;
border: none;
}
resources/js/main.js
Everything was going well so far, except we run into our first problem. We cannot use node
modules here, we only have much more limited set of APIs, and if we need anything beyond that? Too bad.
For our use case we only need one command - Neutralino.os.execCommand
, but no access to node ecosystem makes it far less useful than Electron or NW.js. And we don't even get any security benefit for this restricted access, as those limited commands are totally sufficient to own the machine.
let form = document.querySelector("form")
let input = document.querySelector("input")
let terminalHistory = document.querySelector("#history")
function createInputLine(command) {
let inputLine = document.createElement("div")
inputLine.className = "input-line"
let promptSpan = document.createElement("span")
promptSpan.className = "prompt"
promptSpan.append("$")
let inputSpan = document.createElement("span")
inputSpan.className = "input"
inputSpan.append(command)
inputLine.append(promptSpan)
inputLine.append(inputSpan)
return inputLine
}
function createTerminalHistoryEntry(command, commandOutput) {
let inputLine = createInputLine(command)
let output = document.createElement("div")
output.className = "output"
output.append(commandOutput)
terminalHistory.append(inputLine)
terminalHistory.append(output)
}
form.addEventListener("submit", async (e) => {
e.preventDefault()
let command = input.value
let output = (await Neutralino.os.execCommand({command})).output.trim()
console.log(output)
createTerminalHistoryEntry(command, output)
input.value = ""
input.scrollIntoView()
})
All that changed was no require
line, form submit being async, and (await Neutralino.os.execCommand({command})).output.trim()
replacing previous child_process.execSync(command).toString().trim()
.
Safari
So far it looks like Neutralino is drastically worse than Electron, as you lose access to the whole npm ecosystem, but it would be good enough for simple apps at least?
Hard no.
Unfortunately Neutralino also fails to bundle Chromium, and just uses whatever you have installed on the machine - and for OSX it defaults to Safari, the IE of OSX. So not only you'll suffer from extremely limited APIs, you'll also suffer from all the cross browser incompatibilities.
Should you use Neutralino?
There are zero advantages of Neutralino I can see.
Smaller binaries really don't count - it matters for people accessing websites on shitty phone networks, but if users are downloading your app on a computer, they generally have good network connections, and in any case waiting a few extra seconds for those extra MBs is not a big deal. The difference is less than one TikTok video.
At this point it should be totally clear that you should not use Neutralino. Electron and NW.js both do things a lot better.
Results
Here's the results:
That's it for Neutralino. In the next episode we'll try to check out some other Electron alternatives.
As usual, all the code for the episode is here.
Top comments (0)