Monday, November 13, 2017

Compiling a Node.JS app into a .EXE using PKG step by step walkthrough and hints about zeit pkg and completing dynamic requires using package.JSON

I have a Node.JS app thats very bloated on disk (lot of node.js dependencies) and a huge file tree.
In this guide, we will turn that from:























Using:

https://github.com/zeit/pkg - Node.JS Binary Compiler

"This command line interface enables you to package your Node.js project into an executable that can be run even on devices without Node.js installed."

Start up a command prompt with Node installed and run:
npm install -g pkg
The actual command to compile is one such as:
pkg discordbot --targets latest-win-x64 --debug

Usage:
pkg = this command (npm node command)
entrypoint = the folder of the node.js app, or . for current dir - must find package.json
--targets = nodeRange-platform-arch
    nodeRange node${n} or latest
    -platform freebsd, linux, macos, win
    -arch x64, x86, armv6, armv7
--debug = There is very little output, compilation succeeds but may have omitted resources.

Now is the fun part, you get to find out which errors you had and how to fix them:
The first problem is:
package.json

A) You need to know the basics of JSON.
B) You need additional information in there to make sure it works.
C) Files are going to have to be declared manually, as the tool is not dynamic require aware. This means such things like:
const requireStructure = name => require(`../../structures/${name}`);
or:
function createNpmDependenciesArray (packageFilePath) {
    var p = require(packageFilePath);
...
Aren't going to work.

First step is find package.json in your root tree of your app package dir, and open it in a text editor. Duplicate your "main" line as a "bin" line:
  "main": "discord_bot.js",
  "bin": "discord_bot.js"
This is a sample config:

{
  "name": "DiscordBot",
  "version": "0.1.2",
  "description": "Bot for Discord app modded by genBTC",
  "readme": "README.md",
  "maintainers": [],
  "author": "",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/genBTC/DiscordBot.git"
  },
  "license": "GPL-2.0",
  "dependencies": {
    "8ball": "^1.0.6",
    ...
    "youtube-node": "1.2.x"
  },
  "pkg": {
     "scripts" : ["src/client/websocket/packets/handlers/*.js",
...
        "src/client/websocket/packets/handlers/Ready.js",
...
        "src/client/actions/*.js",
        "src/client/actions/ActionsManager.js"
        ]
    }
}
JSON tip: Note the comma seperated [ list ] of files in "quotes", and with FORWARD / slashes as the path seperator. You can also use * wildcards.

Before we know what files we need to add, we need to compile the program (as shown before with --debug). You can also pipe the output using > result.txt to save it as a text file and look at it later.

C:\Software>DiscordBot.exe
Starting DiscordBot
Node version: v9.0.0
Discord.js version: 10.0.1
pkg/prelude/bootstrap.js:1172
      throw error;
      ^

Error: Cannot find module './handlers/Ready'
1) If you want to compile the package/file into executable, please pay attention to compilation warnings and specify a literal in 'require' call. 2) If you don't want to compile the package/file into executable and want to 'require' it from filesystem (likely plugin), specify an absolute path in 'require' call using process.cwd() or process.execPath.
    at Function.Module._resolveFilename (module.js:540:15)
    at Function.Module._resolveFilename (pkg/prelude/bootstrap.js:1269:46)
    at Function.Module._load (module.js:470:25)
    at Module.require (module.js:583:17)
    at Module.require (pkg/prelude/bootstrap.js:1153:31)
    at require (internal/module.js:11:18)
    at WebSocketPacketManager.register (C:\snapshot\discordbot\node_modules\discord.js\src\client\websocket\packets\WebSocketPacketManager.js:54:21)
    at new WebSocketPacketManager (C:\snapshot\discordbot\node_modules\discord.js\src\client\websocket\packets\WebSocketPacketManager.js:18:10)
    at new WebSocketManager (C:\snapshot\discordbot\node_modules\discord.js\src\client\websocket\WebSocketManager.js:24:26)
    at new Client (C:\snapshot\discordbot\node_modules\discord.js\src\client\Client.js:63:15)
This tells us that it couldnt find "handlers/Ready", and the faulty node_module is "discord.js".
Also, we can open the files indicated in blue and examine the .js scripts for what kind of stuff is being dynamically included, usually we can find a list of stuff such as:

So, we dig around in that dir, and find handlers/Ready (and all the other ones).
If you want to be precise you can:
Open up another command prompt to generate a listing of these files by :
dir /b *.js > list.txt
Then you can use NotePad++ to manipulate the text (add the rest of the path and " ", and form them into the list of files that needs to be passed to package.json.) Hopefully you are fast at this, because there is going to be a lot of files.
Otherwise you can suffice to say you found out that you want to add the entire
   "pkg": {
     "scripts" : ["src/client/websocket/packets/handlers/*.js",
        "src/client/actions/*.js"
        ]
    }

Process Explained:

You are going to repeat the process of Compiling, Looking at the compiler's debug output, Running the .exe and Looking at the binary's output. Then from that - analyze which files need to be included. You want to predict ahead of time and grab entire directories at once.

You can compare the results files, of the before and after - they should indicate success in adding.
C:\Software>DiscordBot.exe
Starting DiscordBot
Node version: v9.0.0
Discord.js version: 10.0.1
logging in with token
(node:49936) [DEP0013] DeprecationWarning: Calling an asynchronous function without callback is deprecated.
Using gateway wss://gateway.discord.gg/?encoding=json&v=6
Connecting to gateway wss://gateway.discord.gg/?encoding=json&v=6
Connection to gateway opened
Identifying as new session
Logged in! Serving in 2 servers
pkg/prelude/bootstrap.js:1172
      throw error;
      ^

Error: ENOENT: no such file or directory, scandir 'C:\Software\resources\default_app\plugins'
1) If you want to compile the package/file into executable, please pay attention to compilation warnings and specify a literal in 'require' call. 2) If you don't want to compile the package/file into executable and want to 'require' it from filesystem (likely plugin), specify an absolute path in 'require' call using process.cwd() or process.execPath.
    at Object.fs.readdirSync (fs.js:924:18)
    at Object.fs.readdirSync (pkg/prelude/bootstrap.js:776:35)
    at getDirectories (C:\snapshot\discordbot\plugins.js:5:15)
....
We've gotten further. This tells us we've logged in now, but it can't find the plugins directory. This is a directory right under our main app dir, and includes
some JSON and non-JSON files. resources\default_app is the internal name of the .exe's file-structure. So we need to include "plugins" into the exe. For this we can use Assets:

"assets" : ["plugins/*"]


If when you try to run the app, you get NPM trying to download and install stuff, its because it wasnt listed in your package.json dependencies. Just go in and manually add it.

After all this, it works!


Thanks for reading. GenBTC here, signing out. Happy Computing. This has been a 2017 genBTC Production.