avatarUday Hiwarale

Summary

The provided web content explains how to create an executable (.exe) file from JavaScript code using Node.js and third-party tools like pkg or nexe.

Abstract

The article delves into the process of converting JavaScript code into a binary executable file for Windows (.exe) by leveraging the Node.js runtime and specialized tools. It clarifies the nature of executable files, differentiating between those for Windows and macOS, and discusses the inclusion of the JavaScript interpreter, program code, and dependencies within the executable. The author guides readers through creating a sample Node.js project, setting up a web server with express.js, and packaging it into an executable using pkg. The process involves bundling static assets, handling file paths with a virtual file system, and targeting specific operating systems and architectures. The article emphasizes the convenience of distributing JavaScript applications as standalone executables, eliminating the need for end-users to install Node.js or manage dependencies.

Opinions

  • The author suggests that pkg is easier to use compared to nexe for creating JavaScript executables, based on popularity trends.
  • Including the Node.js runtime, sources, and dependencies within an executable can lead to a significantly larger file size, which may not be ideal for network transfer.
  • The author implies that for distributing executables, a compiled language like C, C++, or Go might be more efficient than JavaScript due to the size implications of bundling the runtime.
  • The use of a virtual file system (snapshot file system) in the executable is highlighted as a solution for maintaining relative file paths for assets at runtime.
  • The article promotes the idea that packaging a JavaScript application as an executable simplifies deployment and enhances user experience by removing the need for technical setup by the end-user.

Node.js Bundling

How to create an executable (.exe) file from JavaScript code (using Node.js)

In this lesson, we are going to learn how to create a .exe binary executable file from JavaScript code. This is possible with the help of Node.js runtime and some third-party tools.

(source: unsplash.com)

It’s harder to digest the title of this article. How can we possibly create a binary executable .exe file from a JavaScript program if JavaScript is an interpreted language? JavaScript doesn’t even have a compiler so how do we even fathom the idea of generating executables for JavaScript?

A .exe file is generally called a binary executable file. Unlike a normal program file or simply any file with data of a particular format (such as a .png file with image data), a .exe file contains various different things such as actual program code, program data, static resources, dependencies among others. It is meant to execute a task on the system instead of simply being a data carrier.

So a binary executable file is more like a compressed file with different data segments that play different roles to execute the program contained in it. This program can be run when the user simply double-clicks on the file. The native operating system understands the .exe file extension and how to process the file and finally execute the program in it.

The .exe extension in my opinion is a little misunderstood since it is overly generalized. The .exe extension is specific to the Windows operating system. When Windows OS sees the .exe file extension, it knows that the file is a portable binary executable file (written in PE format) and the operating system will execute the file natively when the user double-clicks on it (instead of trying to open the file in an installed application).

Other operating systems may use different extensions. When it comes to macOS, a binary executable file doesn’t have an extension. The OS determines if a file is a binary executable by looking at the first few bytes of the file which we call Magic Numbers. You can see the entire list of valid magic bytes here. The macOS uses CF FA ED FE leading bytes of a file to determine if a file is a 64-bit binary executable. Such files are shown with a terminal icon in the finder as shown below and have Unix executable kind.

(macOS finder window)
(Hex Fiend Dump of the program file)

The macOS operation system uses .app file extension to determine a native application file. Unlike a binary executable file, a .app file is more like a system directory that contains proper folders for different segments of the applications. When the user double-clicks on it, the OS finds Info.plist file inside this file (since its a directory) and runs a suitable program (located inside the same directory) indicated by the Info.plist.

💡 Read this QUORA answer to understand how macOS treats executable and .app files. The same principles are generalized in this answer.

Now that we have some idea of what executable files are and how they work, we can proceed to understand how JavaScript executables can be created.

Since an executable file is more like a compressed file with different data segments your actual program (contained in it) depends on, you can cram absolutely anything inside it such as images, program files, dependencies, or even the entire interpreter that your program would need in order to run.

Since we can’t compile a JavaScript program to machine language (such that it can run natively on the machine), we need to ship the JavaScript program in its true form (source code) along with the JavaScript runtime such as Node which contains the JavaScript interpreter and OS-level dependencies.

Therefore, the final executable contains the JavaScript source code (along with dependencies such as .js files or NPM packages), static resources (such as .json files, images files, etc. if needed), and most importantly the Node.js runtime (executable) to run these JavaScript programs. This executable contains a subroutine (program) that knows how to run included JavaScript programs using the included JavaScript runtime when the user double-clicks on it. And that is how a JavaScript executable works.

But we can’t make such an executable on our own since neither Node.js nor NPM provides any tools to do the same. But there are some popular third-party tools we can use such as pkg and nexe.

But before we have a look at them, let’s first create a sample JavaScript (Node) project that we want to provide to our clients as a binary executable so that they do not need to install Node.js or NPM modules on their own. This is quite helpful as it eliminates the need of having an installation guide which can be frustrating for you and your clients especially to those who do not possess technical knowledge.

💡 However, including sources, dependencies, and a runtime environment inside an executable file makes its size bigger than an executable file compiled from a compiled language such as C, C++ or Go. At times, this file size could be so big that it’s not even feasible to transfer over the network. That’s why you should prefer a compiled language for things like this.

JavaScipt Project

We are going to build a small web application for this demo. When the user double-clicks on the executable file, our executable file will start a web server using express.js and open the root server endpoint (simply /) in the default browser of the system.

This default endpoint returns an HTML page (with external .js and .css files) that render some images of formula one drivers on the screen. To get the list of drivers, the .js file makes an AJAX request to the server using fetch API of the browser.

To make this application, we need some .html, .js, .css and .png files as static assets. We are going to use a simple .json file to provide a standard response for the AJAX request. You should be able to find the source code of this demo in the repository below.

First of all, we need to initialize package.json in the project directory and install all NPM dependencies needed for this application to work.

$ npm init -y
$ npm install -S express get-port open

The express package is what will create a web-server and process incoming requests. The get-port package provides us the available port on the system so that we can start the web-server on that port without any issues. The open package opens an URL in the default browser of the system.

node-js-executable
├── build/
├── package.json
├── server.js
└── src
   ├── static
   |  ├── images
   |  |  ├── alexander_albon.png
   |  |  ├── ...
   |  |  └── valtteri_bottas.png
   |  └── jsons
   |     └── images.json
   └── www
      ├── index.html
      ├── main.js
      └── style.css

Our project structure looks like above. The src directory contains the source code and assets of the web application. The www directory contains the static assets of the web application. When the web application starts (in the browser), the express server is going to serve the index.html by default which imports main.js file to dynamically render images and style.css for styles.

The static directory contains some static assets such as images directory to serve the images for the web application and the images.json file which contains an array of image objects with titles and relative image URLs.

The server.js file is the brain of the application. When we run node server.js command, an http server is started using express.js and it opens http://127.0.0.1 URL in the browser. This is what this webpage looks like.

(http://127.0.0.1:3000/web/)

You should have a look at the repository source code to understand how we are fetching images from the servers and rendering them. Now that our web application is ready, it’s time to create executable from it.

Creating An Executable

For this demo, we are going to use pkg command-line tool. You are free to choose nexe or other of your choice but I find pkg easier to use. First of all, we need to install pkg globally using npm command. You can also install it locally to use the CLI interface programmatically.

$ npm install --global pkg

Once the package is installed globally, you get the pkg command to create executables. The pkg command needs an input file which is an entry JavaScript program that will run when the user double-clicks on the generated executable file. Other command-line options control how the executable file is generated.

Entry File

The entry file path is provided through <input> argument such as the $ pkg [options] server.js command in our case which is a local file path. But we can also provide the path to package.json and the pkg command will use the bin property of the package.json to locate the path of the entry JavaScript file. If a directory path is provided, then the pkg command looks for the package.json file of that directory uses its bin property.

Assets

Assets are static resources such as .html, .js, .css, .png, .json files that should be included in the executable. To instruct pkg to include such files in the executable, we need to provide these file paths through pkg.assets property of the package.json. Its value is a glob pattern or an array of glob patterns.

💡 In this case, the package.json should be the input path so that pkg can pick assets that are needed to be included in the executable.

When an executable is created, pkg will include the input JavaScript file along with static assets in a single executable file. This is where things get a little complicated. Since all project files are now packed into a single file, the relative file paths lose their meaning and so does the folder structure.

But at runtime (when the executable is running), these files are organized into a virtual file system called a snapshot file system. Usually, these files at runtime have /snapshot/ (or C:\snapshot\ in windows) prefix in their path as if they are located inside /snapshot directory of the system.

But you can safely locate an asset file in the executable (at runtime) using the path.join method and __dirname variable to construct a relative path such as path.join( __dirname, './www/main.js' ). The path will work just fine as if you have a real directory path in a real file system. To know more about the snapshot filesystem, read this documentation.

💡 Beware. During the compilation process, pkg looks at the require() statements in the code and automatically include these files as static assets. Therefore, you might not need to specify such files as static assets in the package.json. However, there are some caveats as explained here.

Targets

We can generate an executable file for a specific system using the --targets command-line flag. The value of this flag a string in the <node>-<platform>-<arch> format. It could also be a comma-separated list of this pattern to target multiple system architectures. You could also use the pkg.targets field of the package.json to specify these values.

The node value is a target major version of the Node. The platform value is the name of the target operating system and it could be one of these freebsd, linux, alpine, macos, win. The arch is the target processor architecture and it could be one of these x64, x86, armv6, armv7.

We could omit one of these values from the target specification. In that case, pkg uses its value from the current system configuration. We can use 'host' as the value for the --targets in which case all these values are obtained from the current system configuration.

Output

We will try to create executable files for the macOS and Windows operating systems, especially for x64 processor architecture. As our entry point is server.js, we will use bin property of the package.json to indicate its file path. Also, we will mention all file path inside node_modules and src as static assets since our server.js dependents on it. Considering all these requirements, our package.json looks like below.

To generate the binary executable files, we need to execute pkg . command since we want pkg to use the package.json file from the current directory to locate the input file, assets, and targets. We could also use pkg package.json command which also does the same thing.

Once this command is run, pkg downloads appropriate Node.js binaries based on the targets values and cache them in a directory specified by the PKG_CACHE_PATH environment variable so that next time when the same targets are encountered, pkg could use the cached binary files.

Once all binary files are downloaded, pkg generates the executable files in the current directory. The filename and extension of these files is decided by pkg and normally looks like <package>-<target>.<ext> where package is the name value in the package.json.

However, we can control the naming scheme and the output directory using the --output or --out-path option values. We are going to use --output flag to specify the output directory and name of the executable file.

$ pkg --output build/web .

The command above generates the following files inside build directory.

node-js-executable
└── build
   ├── web-node10-win.exe (37.4mb)
   └── web-node12-macos (43.4mb)

Now you can double-click on any of these files (as per your machine) and it will open a webpage in the default system browser displaying the formula one drivers.

(http://127.0.0.1:52254/)
(thatisuday.com / GitHub / Twitter/ StackOverflow / Instagram)
JavaScript
Nodejs
Software Development
Programming
NPM
Recommended from ReadMedium