Extensions Overview
The Neutralino framework provides a native API that allows users to perform various operating system-level operations such as accessing the filesystem, executing commands, and showing dialog boxes. While users may require additional native APIs like database connectors for building their applications, adding all of them to the core would make the framework bloated. Therefore, the framework offers a WebSocket-based extension system that enables users to extend the Neutralinojs API without having to build the framework from source.
The extensions API provides the flexibility to write custom backend code for your application using any programming language. Furthermore, the extensions API allows you to include the Neutralinojs process as a part of any source file.
Defining the extensions
First, you need to define extensions you use in neutralinojs.config.json
with the following structure.
"extensions": [
{
"id": "js.neutralino.sampleextension",
"commandLinux": "${NL_PATH}/extensions/binary/linux/ext_bin",
"commandDarwin": "${NL_PATH}/extensions/binary/mac/ext_bin",
"commandWindows": "${NL_PATH}/extensions/binary/win/ext_bin.exe"
},
{
"id": "js.neutralino.binaryextension",
"command": "node ${NL_PATH}/extensions/binary/main.js",
}
]
id
String: A unique key to identify each extension. This id cannot contain any characters except for letters, numbers, and periods.command
String (optional): A cross-platform command to start the extension. Eg:node ${NL_PATH}/extensions/binary/main.js
will work on every platform.commandLinux
String (optional): Extension startup command for Linux.commandDarwin
String (optional): Extension startup command for macOS.commandWindows
String (optional): Extension startup command for Windows.
Enable the extensions feature
The extensions API is disabled by default. Enable extensions by adding the following setting to your app config.
"enableExtensions": true
Connecting an extension with Neutralinojs
As you already noticed, an extension is just a separate process. Neutralinojs starts spawning extension instances during the framework bootstrap process and initiates each extension process by sending the following JSON object via standard input streams:
{
"nlPort": "",
"nlToken": "",
"nlConnectToken": "",
"nlExtensionId": ""
}
The above JSON properties contains connectivity information as follows:
nlPort
: port of the Neutralinojs server.nlToken
: Access token to use the native API.nlConnectToken
: A token that extension should send during WebSocket connection initialization.nlExtensionId
: Extension identifier.
Now, you can connect with the Neutralinojs server with the above details. Use the following WebSocket URL to initiate a new WebSocket connection.
ws://localhost:{port}?extensionId={extensionId}&connectToken={connectToken}
Sending a message from app to an extension
The extensions API uses an event-based messaging protocol. Every message uses the following JSON structure.
{
"event": "<event_name>",
"data": {}
}
Use the built-in extensions API to send a message to any extension, as shown below.
let extension = 'js.neutralino.sampleextension';
let event = 'helloExtension';
let data = {
testValue: 10,
};
await Neutralino.extensions.dispatch(extension, event, data);
The above code snippet sends a message to the js.neutralino.sampleextension
extension instance. You can send
messages to extensions with the dispatch
function anytime. If you send a message before the extension connects
with the main process, the Neutralinojs client library queues and sends it when the target extension's connection is
established. In other words, you don't need to worry about extensions' status when you send messages to
extensions.
Sending a message from the extension to app
When you connect your extensions with the Neutralinojs main process, you can call the native API by sending WebSocket messages to the Neutralinojs process directly. Neutralinojs server processes messages based on the following format.
{
"id": "<id>",
"method": "<method>",
"accessToken": "<token>",
"data": {}
}
id
String: A UUID v4 string.method
String: Native method name. Eg:window.setTitle
.accessToken
String: Access token generated by the Neutralinojs server.data
Object (optional): Parameters for the native method.
You can invoke the app.broadcast
native method to send messages to all app instances. Register a callback
with the events.on
in the application code to receive the message send by the extension process.
Terminating an extension instance
When Neutralino exits, it does not send kill signals to all extension instances. Therefore, it is necessary to stop the extension process when the WebSocket-based IPC (Inter-Process Communication) closes. The following Node.js extension code shows how to do this:
const fs = require('fs');
const process = require('process');
const WS = require('websocket').w3cwebsocket;
const { v4: uuidv4 } = require('uuid');
const chalk = require('chalk');
// Obtain required params to start a WS connection from stdIn.
const processInput = JSON.parse(fs.readFileSync(process.stdin.fd, 'utf-8'));
const NL_PORT = processInput.nlPort;
const NL_TOKEN = processInput.nlToken;
const NL_CTOKEN = processInput.nlConnectToken;
const NL_EXTID = processInput.nlExtensionId;
const client = new WS(
`ws://localhost:${NL_PORT}?extensionId=${NL_EXTID}&connectToken=${NL_CTOKEN}`
);
client.onerror = () => log("Connection error!", "ERROR");
client.onopen = () => log("Connected");
client.onclose = () => process.exit();
client.onmessage = (e) => {
const { event, data } = JSON.parse(e.data);
if (event === "eventToExtension") {
log(data);
client.send(
JSON.stringify({
id: uuidv4(),
method: "app.broadcast",
accessToken: NL_TOKEN,
data: { event: "eventFromExtension", data: "Hello app!" },
})
);
}
};
function log(message, type = "INFO") {
const logLine = `[${NL_EXTID}]: ${chalk[
type === "INFO" ? "green" : "red"
](type)} ${message}`;
console[type === "INFO" ? "log" : "error"](logLine);
}
This code implements a simple Node.js extension for Neutralinojs, which establishes a WebSocket connection to the Neutralinojs server and handles incoming messages from the server. It also sends a message to the server using the client.send method when it receives a specific event from the server.
For more information on how to terminate an extension instance, you can refer to the sample extension source. https://github.com/neutralinojs/neutralinojs/tree/main/bin/extensions/sampleextension
Using Neutralinojs from your source files
The above approach helps you to extend Neutralinojs API with a custom backend code. Neutralinojs process can spawn multiple extensions as child processes and communicate with the internal messaging protocol. On the other hand, you can spawn Neutralinojs processes from your own processes and communicate with the same messaging protocol. Using this approach, it's possible to write Neutralinojs apps with any backend language.
You can obtain authentication details for the Neutralinojs process by setting your config as below.
"exportAuthInfo": true
The above setting exports authentication details to ${NL_PATH}/.tmp/auth_info.json
with the following format.
{
"nlPort": "<port>",
"nlToken": "<token>",
"nlConnectToken": "<connect_token>"
}
Connect with the Neutralinojs process by using the extension API as usual with the extension identifier you used in the application configuration file.