🚀 Easily convert your Node.js application into an executable with SEA (Single Executable Application).
Supported on: Windows, macOS, Linux
Made with ❤️ by Klicat - France
npm install --save-dev node2exeIn your project, run:
npx node2exenpx node2exe -VThis generates app-1.0.0.exe (or app-1.0.0 on macOS/Linux) based on your package.json version.
Add to your package.json:
{
"scripts": {
"build:exe": "node2exe"
}
}Then run:
npm run build:exe
npm run build:exe -- -V # with version- Node.js 24+ (with SEA support)
- A file specified in
package.jsonmainfield (e.g.,server.js,app.js,index.js) - A
package.jsonfile
Note: The executable name is based on the main field filename. If not defined, it defaults to app.
- Generates:
app.exe(Windows executable) - Run: Double-click or
app.exe
- Generates:
app(macOS executable) - Run:
./appin terminal - Note: Automatic code signing
- Generates:
app(Linux executable) - Run:
./appin terminal
- ✅ Reads entry point from
package.jsonmainfield - ✅ Installs
postjectif not already present - ✅ Creates
sea-config.jsonautomatically - ✅ Generates the SEA blob
- ✅ Creates the executable for your platform
- ✅ Cleans up temporary files
# Installation
npm install --save-dev node2exe
# Usage
npx node2exe
# Result
# ✅ app.exe created! (Windows)
# ✅ app created! (macOS/Linux)app.exe/app- Your final executable (ready to distribute)sea-config.json- SEA configuration (optional after creation)node_modules/- Contains postject and dependencies
require), not ES Modules (import).
However, with node2exe's automatic bundling, you can write in ES Modules! Here's how:
- You write ES Modules:
import colors from 'colors';
import https from 'https';
console.log('Hello'.blue);- esbuild automatically converts to CommonJS:
const colors = require('colors');
const https = require('https');
console.log('Hello'.blue);- SEA runs the CommonJS version in the executable
Don't forget to add "type": "module" to package.json:
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"main": "server.js"
}Your code:
import express from 'express';
import colors from 'colors';
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
console.log(colors.blue('→ Request received on /'));
res.send('Hello World from Express!');
});
app.listen(PORT, () => {
console.log(colors.green(`✓ Server running on http://localhost:${PORT}`));
});{
"name": "my-app",
"version": "1.0.0",
"main": "server.js"
}Your code:
const express = require('express');
const colors = require('colors');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
console.log(colors.blue('→ Request received on /'));
res.send('Hello World from Express!');
});
app.listen(PORT, () => {
console.log(colors.green(`✓ Server running on http://localhost:${PORT}`));
});Some advanced ES Module features may not work:
- Dynamic imports -
import()at runtime - Top-level await -
awaitoutside async function - Circular dependencies - complex import cycles
Simple rule: If your imports look straightforward, they'll work!
Official documentation: https://nodejs.org/api/single-executable-applications.html
"The single executable application feature currently only supports running a single embedded script using the CommonJS module system."
The name of your executable is automatically determined by the main field in your package.json:
{
"name": "my-project",
"main": "server.js",
"version": "1.0.0"
}Running npx node2exe creates: server.exe (or server on macOS/Linux)
{
"name": "my-project",
"version": "1.0.0"
}Running npx node2exe creates: app.exe (default name, since main is not defined)
{
"name": "my-project",
"main": "index.js",
"version": "2.5.0"
}Running npx node2exe -V creates: index-2.5.0.exe
- node2exe reads the
mainfield frompackage.json - Extracts the filename without extension:
server.js→server - Uses that as the executable name
- If
mainis not defined, defaults toapp - With
-Vflag, appends the version:server-1.0.0.exe
- The created executable includes all your code and Node.js
- No external dependencies required to run
- Typical size: 60-80 MB depending on your app
- The script is written in pure JavaScript (cross-platform)
- Each platform generates its own executable
- Use the
-Vflag to include version in the filename
When building on Windows, you may see this warning during the injection step:
warning: The signature seems corrupted!
This is completely normal and harmless!
Here's why it happens:
- Windows signs all
.exefiles with a digital signature for security - When
postjectinjects the SEA blob, it modifies the binary structure - This breaks the original Windows signature (but the file still works perfectly)
- The warning is just informational and can be safely ignored
The executable will work perfectly fine without the signature. If you want to sign it with a code signing certificate, you can use signtool from the Windows SDK (requires a valid certificate).
⏳ Important: The injection step (step 4/5) can take 30-60 seconds depending on your disk speed.
This is because postject needs to:
- Read the entire Node.js binary (50-80 MB)
- Inject the SEA blob into it
- Write the complete file back to disk
This is normal and expected. Just wait for the process to complete. Do not interrupt it during this step!
When you have npm packages in your node_modules, node2exe automatically bundles them into the executable using esbuild. This works great for most packages!
npm install colors
npx node2exe
# This automatically bundles 'colors' into the executableSome npm packages require external data files (like databases or configuration files). These cannot be automatically bundled into the executable because:
- SEA doesn't support file system access for bundled assets by default
- Data files are not code - esbuild bundler only handles JavaScript
- Path resolution fails - packages looking for files on disk won't find them in the executable
Examples of packages with data files:
geoip-lite- requires.datfiles- SQLite databases -
.dbfiles - Font files, images, config files
Solutions:
-
Use online APIs instead - Replace local data with API calls (recommended) ✅
// Instead of: const geoip = require('geoip-lite'); // Use: fetch('https://ip-api.com/json/' + ip)
-
Use web services - Move data-heavy operations to cloud services
-
Manually add assets - Edit
sea-config.jsonto include assets (advanced, complex)