Distribute application with Deno
Introduction
Deno isn’t simply a runtime to run Typescript and JavaScript applications. It is also a complete tool chain. Deno has several useful commands to help with the development process. In addition to helping developers, Deno also has a couple of commands to help in distributing the application.
These commands are:
- bundle: Create a single JavaScript file for the application. This is useful when the application need to sent as an open code JavaScript file. This is more suitable for distributing an open-source library.
- compile: Create a self-contained executable for the application. This is useful when the application need to sent without sharing the code. This is more suitable for distributing an application in trusted environments.
Both of the commands are useful in distributing the application or library. The way of distribution depends on the distributor and the receiver. If the distributor wants to share the application, but doesn’t want people to be able to read the code, a self-contained executable is better. However, self-contained executables are often viewed as security-risk. If the distributor is okay with people to be able to read the code, a single JavaScript file is best. Usually open-source libraries would be distributed as a single JavaScript file.
In this article, we’ll learn how these two ways of distribution works with Deno.
Bundle
Bundling is a way to distribute the entire application with all the dependencies by creating a single JavaScript file. The output is JavaScript, not TypeScript as TypeScript, despite its great advantages, is not widely supported (not yet at least). The bundle command produces a JavaScript that can be read by anyone, therefore it’s trustworthy. But the code is visible to all.
Here are the pros and cons of using the bundle command:
Command
The bundle command is very simple. It takes the main program or main module (like index.js or mod.ts) as input, processes it and the dependencies. It then outputs a single JavaScript file.
deno bundle <root-module.ts> <output-file.ts>
That’s all! There are some advanced options like no-check, no-remote, etc. but those aren’t common to use.
If output-file.ts isn’t provided, then the bundled output is written on console.
Examples
For the first example, let’s take a simple app and see what happens. Here is a very simple program that adds 100 to a given number.
//app.tsfunction f(i: number) {
return i+100;
}
console.log(f(1));
Here is the output of the bundle command:
> deno bundle app.ts Check file:///private/var/tmp/a.ts
Bundle file:///private/var/tmp/a.ts
function f(i) {
return i + 100;
}
console.log(f(1));
As mentioned, the output would be JavaScript as TypeScript isn’t accepted widely.
In the next example, let’s take a file with some dependencies:
//app.ts
import * as a from "./a.ts"
import {delay} from "https://deno.land/std/async/mod.ts"await delay(a.f(500));//a.ts
export function f(i: number) {
return i+100;
}
Let’s bundle it and see what happens:
> deno bundle app.ts app.js//app.jsfunction f(i) {
return i + 100;
}
function delay(ms) {
return new Promise((res)=>setTimeout(()=>{
res();
}, ms)
);
}
await delay(f(500));
The bundle command created a single JavaScript file by resolving all the dependencies. The output is a single standalone JavaScript file. A single file is easy to distribute.
Finally, let’s create a library that would return a v1 and v4 UUID:
//app.ts
import * as u from "https://deno.land/std/uuid/mod.ts"export function get() {
return u.v4.generate()+'-'+u.v4.generate();
}export function getV1() {
return u.v1.generate()+'-'+u.v1.generate();
}
This is a good example for distribution. This can be bundled in a single JS file for easy distribution:
deno bundle app.ts app.js
Check file:///private/var/tmp/app.ts
Bundle file:///private/var/tmp/app.ts//
.... code omitted as it's too long ....//wc -l app.js
//495
The app.js contains a total of 495 lines of code with no dependencies. As a next step, it can be minified for a more compressed output.
Compile
The only problem with bundle command is that the code is open. The compile command hides the code by creating a self-contained executable for a variety of platforms. This executable can be distributed easily. However, there would always be a trust issue with executables. The compile command supports cross-compilation i.e. it can build executables for other platforms too.
There are three supported platforms:
- x86_64-unknown-linux-gnu (General 64-bit Linux)
- x86_64-pc-windows-msvc (64-bit Windows)
- x86_64-apple-darwin & aarch64-apple-darwin (64-bit Mac)
As the compile command builds executables, permissions are important! All the access (read, write, net, env, etc.) need to be specified at the compile time. If not specified, the application may result in run-time error.
deno compile <permissions> <root-module.ts> <output>
- Permissions: Depending on the application, permissions must be enabled at compile time.
- Root module: The main program or the main module
- Output: The name of the executable (If unspecified, Deno tries to guess a name from input)
The compile command produces an executable that hides the code, but executables are considered risky when compared to open code. The other issue with the compile command is that the executable size is large.
Examples
We’ll use the same examples that we’ve seen for the bundle command.
For the first example, let’s try a very simple one:
//app.tsfunction f(i: number) {
return i+100;
}
console.log(f(1));
Now, let’s compile and run it:
> deno compile app.ts
Check file:///private/var/tmp/app.ts
Bundle file:///private/var/tmp/app.ts
Compile file:///private/var/tmp/app.ts
Emit app> du -kh app
77M app> ./app
101
The size of executable is 77M. That’s too much! As mentioned earlier, one of the problems with compile is the size of the executable. Hopefully Deno would fix it soon.
Next, let’s try with a more complex example:
//app.ts
import * as a from "./a.ts"
import {delay} from "https://deno.land/std/async/mod.ts"
await delay(a.f(500));//a.ts
export function f(i: number) {
return i+100;
}
Let’s try to compile it:
> deno compile app.ts
Check file:///private/var/tmp/app.ts
Bundle file:///private/var/tmp/app.ts
Compile file:///private/var/tmp/app.ts
Emit app> du -kh app
77M app
The size stays same regardless of the addition of new imports.
Next, let’s try to do a cross-compilation for linux:
> deno compile --target x86_64-unknown-linux-gnu app.ts
Check file:///private/var/tmp/app.ts
Bundle file:///private/var/tmp/app.ts
Compile file:///private/var/tmp/app.ts
Checking https://dl.deno.land/release/v1.10.2/deno-x86_64-unknown-linux-gnu.zip
Download has been found
Archive: /var/folders/k0/3447gbp16vl309gg50ygclwr0000gn/T/.tmpL0I5id/deno.zip
inflating: deno
Emit app> du -kh app
77M app
Next, let’s try for windows (the extension is exe for windows):
> deno compile --target x86_64-pc-windows-msvc app.ts
Check file:///private/var/tmp/app.ts
Bundle file:///private/var/tmp/app.ts
Compile file:///private/var/tmp/app.ts
Checking https://dl.deno.land/release/v1.10.2/deno-x86_64-pc-windows-msvc.zip
Download has been found
Archive: /var/folders/k0/3447gbp16vl309gg50ygclwr0000gn/T/.tmpOHamID/deno.zip
inflating: deno.exe
Emit app
> du -kh app.exe
54M app.exe
Size comparison
Before closing, let’s do a quick size comparison with other languages/runtimes. We’ll compare between Deno, C++, and Go.
Here is the code in each of them:
//======== Deno code ========
export function f(i: number) {
return i+100;
}console.log(f(1));//======== C++ code ========
#include <iostream>
int f(int a) {
return a+100;
}int main() {
printf("%d", f(1));
}//======== Go code ========
package mainimport "fmt"func f(num int) int {
return num+100;
}func main() {
fmt.Printf("%d", f(1));
}
Here is the size comparison:
As expected:
- C++ is the most compact (just 16K)
- Go is good too (2.1M)
- Deno is way too much (77M)
This story is a part of the exclusive medium publication on Deno: Deno World.