In recent days, I had the opportunity to revisit and reorganize the tools we use to set up our development environments. When we start a new development project, we always need two things:
- A web server (usually Nginx);
- A proxy to quickly share local or test instances both publicly and internally.
In addition to those two, many other secondary tools are currently all managed through a Node.js CLI tool. Hence my concern: before even being able to run the configuration tools, there is the need to install Node.js.
"And what if I wanted to get rid of this additional initial step as well?" — No one
In the past, I had already experimented with pkg (now archived), so I decided to dedicate a day to updating myself on the topic and improving our tools.
Step 1: Creating the bundle
The first step towards creating an executable is to generate a bundle of the CLI tool. I've delved into the tool I know best: Webpack.
Given that build efficiency and speed are not variables of our problem, Webpack allows me, with a couple of loaders, to quickly create a single entry point:
File webpack.config.js
...
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.(node)$/,
loader: "node-loader",
},
...
In a few minutes, the bundle task is solved; we add a script to the package.json, and we are ready to proceed.
File package.json
"scripts" : {
...
"bundle": "webpack",
...
}
Step 2: Creating a .blob file
The next step, now that we have our cli.js bundle in the dist folder, is to generate a .blob file that we will use to create our executable file. To do this, the first step is to define within a configuration file:
- the entry point of our .blob (i.e., the bundle just created);
- the output where the .blob file will be generate.
File seaconfig.json
{
"main": "./dist/cli.js",
"output": "./dist/cli.blob",
}
Once the configuration file is ready, the next step is to create our executable... here all the credit goes to Chad R. Stewart for saving me a lot of work. Here's a link to his article where he goes into more detail for each platform.
I will report here only the steps I had to execute on my Ubuntu machine:
- Generate the .blob file:
node --experimental-sea-config seaconfig.json
Prepare the Node.js executable into which we will inject our file in order to prepare the final file.
Note: Currently, the version of Node.js I'm working with is 21.7.1.
cp $(command -v node) ./dist/server-tools
- Finally, we inject the .blob file into our Node.js executable to complete the task (Depending on the platform, refer to the article linked above):
npx postject ./dist/server-tools NODE_SEA_BLOB ./dist/cli.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
At this point, I was ecstatic to have obtained a perfectly functioning executable of my CLI tool... until...
Step 3: Managing Static Assets
Testing it, I realized that, being a configuration tool, it used various static files as configuration templates, and those static files were not included inside my executable.
Fortunately, while researching how to work around this problem, I came across two useful resources:
With just a few modifications to the configuration file, I included the necessary files in the executable:
File seaconfig.json
{
"main": "./dist/cli.js",
"output": "./dist/cli.blob",
"assets": {
"default.txt": "./templates/default.txt",
"default--ssl.txt": "./templates/default--ssl.txt",
"next-js.txt": "./templates/next-js.txt",
"next-js--ssl.txt": "./templates/next-js--ssl.txt",
"patched.txt": "./templates/patched.txt"
}
}
At this point, the last remaining step was to fix the code (in a very raw and quick way) from:
File addTemplate.ts
import { readFileSync } from "fs";
...
const content = readFileSync(resolve(__dirname, "templates", `${template}${ssl ? "--ssl" : ""}.txt`);
to:
File addTemplate.ts
import { getAsset } from "node:sea";
import { readFileSync } from "fs";
...
const content = (() => {
try {
return readFileSync( resolve(__dirname, "templates", `${template}${ssl ? "--ssl" : ""}.txt`), "utf-8" );
} catch (error) {
return getAsset(`${template}${ssl ? "--ssl" : ""}.txt`, "utf-8");
}
})();
Only note: I also had to add a declaration to ensure that TypeScript didn't complain about the missing module:
File types.d.ts
declare module 'node:sea' {
export function getAsset(filename: string, encoding: string): string;
}
Final thoughts
By the end of the day, I successfully achieved the desired outcome: My executable functions flawlessly on a pristine VPS without the need to install Node.js in advance.
Top comments (0)