Introduction
Node.js 20 was released very recently. Along with several other features, you can now compile your Node.js project into a single executable that can be run in environments without Node.js installed. It’s important to note that this is still experimental and may not be suitable for use in production.
Node.js has instructions on how to set up these single executables: https://nodejs.org/api/single-executable-applications.html
Unfortunately, when compiling the executable, you will not compile dependencies into your executable. To solve this problem, we will leverage a JavaScript bundler to bundle our dependencies into one file before compiling it into our single executable.
Prerequisites:
Node.js 20
Please note: While TypeScript is used in this article, it is not necessary.
Putting Together our Project
First, we need a project that we will build into our executable.
We’ll first define our server.
server.ts
import express from "express";
import https from "https";
import fs from "fs";
import path from "path";
export const app = express();
//Initialize Request Data Type
app.use(express.json({ limit: "10mb" }));
app.get("/", (req, res) => res.send("Hello World!!"));
const port = 3000;
app.listen(port, () => {
console.log(`Server is live on ${port}`);
});
We define our package.json next:
package.json
{
"name": "node-executable-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "esbuild server.ts --bundle --platform=node --outfile=server-out.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"esbuild": "0.17.17",
"express": "^4.18.2"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^18.15.7",
"typescript": "^5.0.2"
}
}
Please note the build script. Esbuild will take our .ts
file and bundle it with our dependencies into a single .js
file server-out.js
. You can actually run this file once it is created using node server-out.js
to check if the bundling was done correctly.
We then define our tsconfig.json
tsconfig.json
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022"],
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
We now define our sea-config.json file.
This is a configuration file building a blob that can be injected into the single executable application (see Generating single executable preparation blobs for details)
sea-config.json
{
"main": "server-out.js",
"output": "sea-prep.blob"
}
Now that we have everything we need, we can begin putting together our single executable.
Creating the Single Binary File
Begin by installing all the dependencies we’ll need by running this command:
npm install
Once npm install is completed, we run the command:
npm run build
This will create our server-out.js
which will be our bundled file we will make into an executable.
Note: If you rather, you can follow the instructions from the Node.js guide starting from step 3 as the following steps will be exactly the same, located here: https://nodejs.org/api/single-executable-applications.html
Generate the blob to be injected:
node --experimental-sea-config sea-config.json
Create a copy of the node executable and name it according to your needs:
cp $(command -v node) server
Note: If you are on a Linux Distro (such as Ubuntu), you can skip the next steps and move straight to running the binary.
Remove the signature of the binary:
On macOS:
codesign --remove-signature server
On Windows (optional):
signtool can be used from the installed Windows SDK. If this step is skipped, ignore any signature-related warning from postject.
signtool remove /s server
Inject the blob into the copied binary by running postject with the following options:
-
server
- The name of the copy of the node executable created in step 2. -
NODE_SEA_BLOB
- The name of the resource / note / section in the binary where the contents of the blob will be stored. sea-prep.blob - The name of the blob created in step 1. -
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
- The fuse used by the Node.js project to detect if a file has been injected. -
--macho-segment-name NODE_SEA
(only needed on macOS) - The name of the segment in the binary where the contents of the blob will be stored.
To summarize, here is the required command for each platform:
On systems other than macOS:
npx postject server NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
On macOS:
npx postject server NODE_SEA_BLOB sea-prep.blob \
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 \
--macho-segment-name NODE_SEA
Sign the binary:
On macOS:
codesign --sign - server
On Windows (optional):
A certificate needs to be present for this to work. However, the unsigned binary would still be runnable.
signtool sign /fd SHA256 server
Run the binary:
./server
You should now have a running Node server similar if you just ran node server-out.js
If you wanted to see a completed example, go here: https://github.com/chadstewart/ts-node-executable-article-example
- If you found this article interesting, please feel free to heart this article!
- If you’re interested in learning more about Front-End Engineering, follow me here on Dev.to and Twitter
- If you’re looking for jobs, I’d highly recommend checking out @TechIsHiring on Twitter, LinkedIn or TechIsHiring's website https://www.TechIsHiring.com/ for posted jobs and other resources!
- Want to check out a curated list of jobs, job seekers and resources from TechIsHiring? Check out TechIsHiring's Newsletter
Top comments (5)
Nice well structured read but it is sending users down a rabbit hole that will not get support (potentially ever). SABs are not meant for bundling dependencies, if you want that you can just use npmjs.com/package/pkg - if this is just a demonstration, at least for security purposes, warn people that it will most likely fail in complex scenarios. In your post you mention node modules as a "problem" that can be solved, when it was designed like that due to multiple reasons, one good discussion I can link is github.com/nodejs/single-executabl...
Have you run into any of these limitations bundling a real life application?
Great, Can you please share the final single executable app size? (for your example code)
I followed their steps for a simple node cli app I made. the node binary itself was 95MB, my code bundle itself (incl all deps) was 1.5MB, and the final executable was 88MB.
Thank you, my experience is similar with other applications.
thanks share