Node.js 🐢, the asynchronous event-driven JavaScript runtime, has unparalleled support for file-system access, among other things - opening up the door to endless possiblities! However, Node.js often loses out to other runtimes/languages in cases where being able to package a single, executable application simplifies distribution and management of what needs to be delivered.
While there are components/approaches for doing this, they need to be better documented and evangelized so that this is not seen as a barrier for using Node.js in these situations. This is important to support the expansion of where/when Node.js is used in building solutions.
This article addresses 2 major concerns in the Node.js ecosystem: bundling and packaging. Let's talk about them briefly.
Bundling is the concept of merging the code, and all its dependencies into a single file. This is commonly seen for frontend development.
However, using the ESM packaging format has one advantage than CJS: tree-shaking. Tree-shaking is the concept of removing unused code from a dependency. Tools: esbuild, parcel, webpack, rollup, terser.
Packaging in Node.js is concept of creating a single executable binary, which includes the source code and the Node.js runtime. This way, Node.js will not be needed to be installed on end-user's machine.
During the process, the tool parses the source code, detects calls to require(), traverses the dependencies, and includes them into executable. Usually the source code is compiled into bytecode using the V8 engine. Tools: pkg, ncc, nexe.
esbuild to bundle
- An extremely fast JavaScript and CSS bundler and minifier
- Most convenient
- Fastest in comparison
- Support for TypeScript syntax, ESM, and CJS
- Supports tree-shaking for ESM
- Supports minification and source maps
# Output CommonJS bundle
$ npx esbuild index.js --bundle --outfile=build.cjs \
--format=cjs --platform=node
# Output ESM bundle
# Note that, you may not need the --banner flag.
# But, in some cases, require() and __dirname are needed.
$ npx esbuild index.js --bundle --outfile=build.mjs \
--format=esm --platform=node --banner:js="
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));"
pkg to package
- Package your Node.js project into an executable
- Instantly make executables for Windows, Mac, Linux, etc
- No need to install Node.js, or hundreds of dependencies
# Packaging tools work best with CJS.
# These tools don't go well with ESM.
# To package into executable, just take the file outputted
# by `esbuild`, and pass it to `pkg`, and we're done!
$ npx pkg build.cjs
This command will output 3 binary exectuable files build-linux, build-macos, and build-win.exe. You might want to run the executable file for your platform. Now you can simply distribute these files to your end-users or deploy in production - without installing Node.js or any dependencies or anything - just this one file!
Thanks for reading! Found it interesting? Give it a ❤️ or 🦄! Any topic you'd want to me cover? Let me know in the comments.
Have a great day!
Top comments (13)
Interesting. Thanks for sharing
Thanks @mhm13dev! I'm happy to know!
I’ve ended up using caxa. Pkg didn’t work for my setup.
Is this works for macos?
Yes. I use it on a MacBook Pro M1 system
hmm, I always did not like that
pkgonly understand require statements, I never thought of using a compiler such as esbuild first.@bias Another option would be to use TypeScript, and output CJS.
did not know this, thanks so much
🚀 @tobecci You're always welcome!
I tried it with a simple example. It works. Really good. Thanks!👍
That's fabulous 🎉 @othimar ! Wish you the best on your journey!
Great idea to use Esbuild before Pkg! Because when I used Pkg straight to my project JS-file I always had issues with path and includes. Now it works from the first try! Thanks for suggestion.
I love you dude!!!! This is really help me...