In this blog post we show you how to compile a small Node.js web server into multi-platform binaries and display content from a external text file on the web page served by this web server.
Overview:
- Setting up the web server
- Compile into binary files
- Display content of a text file
You should understand JavaScript, Node.js and npm. The basics are not explained in this blog post.
Motivation
The resulting applications are
- standalone: no external dependencies
- portable: no installation of Node.js or modules
- simple: only one file to (copy-)deploy
- independent: application can run no matter the installed Node.js version
Developing the sample application: a simple web server
Preparations
Make sure the correct version of Node.js and npm is installed:
> node --version v10.16.0 > npm --version 6.9.0
Create a new folder for the server (myServer
).
Initialize the project inside the new folder with npm init
.
Development dependencies
- StandardJS for linting.
- pkg to compile the Node.js application into cross platform binary files.
> npm install --save-dev standard pkg
Production dependencies
- fastify as web server.
> npm install --save fastify
Server
Add a server.js
file with the following content:
// Require the framework and instantiate it const fastify = require('fastify')({ logger: true }) const path = require('path') const fs = require('fs') // Declare a route fastify.get('/', async (request, reply) => { return { hello: 'world' } }) // Run the server! const start = async () => { try { await fastify.listen(3000) fastify.log.info(`server listening on ${fastify.server.address().port}`) } catch (err) { fastify.log.error(err) process.exit(1) } } start()
Update package definition
Change the main
file in the package.json
to be server.js
and add a lint script.
The package.json
should now look like this:
{ "name": "myserver", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "lint": "standard", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "MIT", "devDependencies": { "pkg": "^4.4.0", "standard": "^12.0.1" }, "dependencies": { "fastify": "^2.4.1" } }
Run lint and start server
Now we can use npm run lint
for linting and npm start
to start the server.
If the lint command returns nothing, there was no problem found.
Starting the server should result in the following output:
> npm start > myserver@1.0.0 start [...]\myServer > node server.js {"level":30,"time":[time],"pid":[pid],"hostname":"[hostname]","msg":"Server listening at http://127.0.0.1:3000","v":1} {"level":30,"time":[time],"pid":[pid],"hostname":"[hostname]","msg":"server listening on 3000","v":1}
Test if the server is working by navigating with your browser to http://127.0.0.1:3000. If you are greeted with „hello world“ the server is working correctly.
Compile the application into binary files
Let’s compile this small web server into binary files for Windows, MacOS and Linux.
First we add a target folder for our binary files: bin
.
To compile the Node.js code to binary files we use the npm package pkg. pkg requires an entrypoint to our application, which can be defined inside package.json
with the bin
property.
While editing the package.json
we can also add a script
entry which compiles the server. The compile script includes a parameter to specify the output path and a .
to provide a entrypoint, this package.json
’s bin
property.
{ "name": "myserver", "version": "1.0.0", "description": "", "main": "server.js", "bin": "server.js", "scripts": { "lint": "standard", "test": "echo \"Error: no test specified\" && exit 1", "compile": "pkg --out-path bin ." }, "author": "", "license": "MIT", "devDependencies": { "pkg": "^4.4.0", "standard": "^12.0.1" }, "dependencies": { "fastify": "^2.4.1" } }
We can compile the server now with npm run compile
and get the following output:
> npm run compile > myserver@1.0.0 compile [...]\myServer > pkg --out-path bin . > pkg@4.4.0 > Targets not specified. Assuming: node10-linux-x64, node10-macos-x64, node10-win-x64
Because we did not specify a binary target, pkg is assuming the targets node10-linux-x64
, node10-macos-x64
and node10-win-x64
, which is fine for our purpose. If you want to specify the target yourself you can use a combination of Node.js version, platform and architecture
Inside the bin
folder you can now find three new files:
myserver-linux
myserver-macos
myserver-win.exe
Start the one for your machine’s architecture from terminal / command prompt and the server should be available, compiled as binary file.
The binary files could now be deployed to any machine, but we want to go a step further and display some content from outside the binary file on the homepage.
Display content of a text file
In this last step we want to read a text file while the server is running and be able to edit the text without restarting the server.
Before we can implement this feature it is important to understand how pkg
is packing files into the packed binary application. Our server.js
entrypoint already contains multiple require
statements. pkg will follow those require
statements and include any required file into the snapshot filesystem. For our text file we want a different behaviour. We want to be able to change the text after the binaries are created, so the text file can not be included inside the binary files. The current working directory (cwd) of the process can help us in this case.
First create a text file named myText.txt
and write some text inside it.
Next, we need to update the „home“ or „/“ route of server.js
to read the text file and display it in the „hello world“ response:
[...] // Declare a route fastify.get('/', async (request, reply) => { let myText const cwd = process.cwd() const myTextRelativePath = './myText.txt' const myTextPath = path.join(cwd, myTextRelativePath) myText = fs.readFileSync(myTextPath).toString() return { hello: 'world', myText: myText } }) [...]
This implementation will read myText.txt
on every page load, which is not optimal, but sufficient for our purpose, because any change to the text can be seen after a page reload. Notice how process.cwd()
is used to get the path of myText.txt
. cwd
is the way to get outside the snapshot filesystem without providing an absolute path.
With cwd
in place, we can now start the server with npm start
and should see our text included in the response, before compiling the server again.
{ "hello": "world", "myText": "This text is read from a TXT file." }
Next we compile the server again with npm run compile
. Now it is important to start the server from the project root directory, because myText.txt
is loaded relative to the cwd
of the terminal / command prompt:
> .\bin\myserver-windows.exe
You can also try to start the binary file from another directory, not including myText.txt
. You should receive the following error while browsing the home page:
{ "statusCode": 500, "code": "ENOENT", "error": "Internal Server Error", "message": "ENOENT: no such file or directory, open '[...]\\myText.txt'" }
You can also change the text inside myText.txt
with the server binary running and after a page reload the change is displayed in the browser.
Conclusion
We created a small web server and compiled it using pkg
. To load a file from the real file system (i.e., not the snapshot filesystem) of the machine, we used the current working directory to escape the snapshot file system of pkg
.
Although our server example is very simple, the presented behaviour is often used to provide configuration files with a binary for example by changing myText.txt
into config.ini
and parse the content.