Making Desktop GUI Applications II - Using DenoCEF, Electric, and Yolk

In a previous article I explored making desktop GUI applications using Deno Webview. Deno Webview is a small library that uses default rendering engines and browsers on various platforms to render a GUI using web technologies. Since it uses different rendering engines on each platform, each using a different JavaScript engine, it is possible to have graphical and performance differences on each platform that renders complex applications difficult to use and debug across multiple platforms. Additionally, if the dependencies for the webview are not installed by default, the program will not be usable until they are installed, making it difficult to ship cross platform applications as self-contained programs. As a result, there is a need to have an option to ship a cross platform rendering engine with your application so that it can run on a common base technology stack across all platforms. And that's where DenoCEF comes in!

DenoCEF is a library and a set of precompiled binaries for making desktop GUI applications using the Chromium Embedded Framework and the Deno runtime. The Chromium Embedded Framework uses similar rendering and JavaScript engines used within the Google Chrome browser. Users of technologies such as Electron will be familiar with these technologies, as similar technologies are used within that project.


How Does DenoCEF Work?

DenoCEF works by using a precompiled Chromium Embedded Framework as a front-end to your application and a server running on the Deno Runtime as a back-end. The Chromium Embedded Framework ships with an HTML parser, a CSS rendering engine (based on the Blink rendering engine used on Google Chrome), and a JavaScript Engine (based on the V8 engine used on Google Chrome). The Deno runtime is a TypeScript and JavaScript runtime based on the V8 engine. These two technologies interact with each other in DenoCEF by means of acting as a client and server, using CEF as the client and Deno as the web server. Since DenoCEF allows you to use a generic web server to interact with your application, virtually any Deno-based web server can be used.

Browser-based technologies, like the Chromium Embedded Framework, run in a sandbox to improve security of the web browser. Combine this with the Deno runtime, which also runs in a sandbox and allows you to specify granular control over what your Deno server has access to on your machine, and you can effectively create a sandboxed application using the same server-side based code on your own local machine. This is done by restricting your DenoCEF application to only interacting with the Deno server on your local machine, pushing the security burden primarily on the Deno runtime to solve. Since Deno gives you granular control over what you allow the runtime to access, the Deno server can be restricted to only have access to the network on the local machine and only to directories within the project directory of your DenoCEF project. Below is a diagram showing the interactions amongst these technologies:




Understanding the Technology Stack

This tutorial uses 3 projects to create desktop applications. The three projects and brief descriptions are:


Basic DenoCEF Application Using the Electric CLI

To get started with DenoCEF, we'll download and use the Electric CLI. This CLI is included within the DenoCEF repository, and can be downloaded with the following command:

Command Line Shell
	deno install -A -f --unstable https://raw.githubusercontent.com/denjucks/deno-cef/master/electric.js
Setting the Path for Deno Installs:

Once downloaded, we can create a new DenoCEF project in the current directory with the following command:

Command Line Shell
	electric create

This command will take some time to run, as internally this command is downloading the DenoCEF binaries from the repository, decompressing the binaries, unarchiving the binaries, and then finally copying the binaries from the cache to the local directory. Since this command caches the binaries, the next time you run this command it will run much quicker.

Refreshing Your DenoCEF Cache

Once the command is finished, you'll see the following files in your folder (it may differ slightly depending on which platform you are on):

Project Structure
	├── app.js
	├── deno
	├── /denocef
	├── grant-permission.sh
	├── README.txt
	├── run
	└── server.js

Essentially, all of the DenoCEF binaries and APIs are located within the denocef folder, with the rest of the folder containing files containing a basic server and some code to run your application. We can test our application by double clicking the run.vbs file on Windows, or by running the ./run command on Linux. A window should pop up like this:



Prequisite Command on Linux

This window is essentially a lighter version of a web browser using similar CSS rendering and JavaScript engines as the Google Chrome browser. The response it received was from the web server created in the server.js file. We can verify this by looking at the contents of the server.js file:

server.js
	import { Application } from "https://deno.land/x/oak/mod.ts";

	const app = new Application();
	
	app.use((ctx) => {
	  ctx.response.body = "<html><body style='display: flex'><h1 style='margin: auto; text-align: center'>Hello world!<br><br>This is a DenoCEF application!</h1></body></html>";
	});
	
	// Include a command line argument to pick the port, as DenoCef uses this
	// command for the Cef.
	await app.listen({ 
	  port: Number.parseFloat(Deno.args[0]) 
	});	

This file contains a basic Oak server sending a generic response to any route on the server. You can see the same text displayed on the CEF instance as is in the context response body of this Oak server. What's different about this server is that, due to permissions set by the DenoCEF API, it only has access to network features on the local network, and has no permissions to read or write by default.


Combining Yolk with DenoCEF

Let's delete the basic server.js file and create a new server using the Yolk CLI. Yolk allows us to generate Oak-based servers, creating a scalable and maintainable structure for those servers, and includes dynamic links to a series of libraries that are commonly used when creating web servers. To install the Yolk CLI, run the following command:

Command Line Shell
	deno install -A -f --unstable https://raw.githubusercontent.com/denjucks/yolk/master/yolk.js

Once it has finished downloading and installing, we can create a new server with the following command:

Command Line Shell
	yolk createproject

This will create several folders and files within our folder, including a new server.js file that will contain the entry point to our server. Yolk takes an applet based approach to web application project structure, where logical pieces of a web application are broken up into applets that contain all the routes, views, and controllers for the specific applet. It also includes and installs a series of middleware for our application. Let's run the server and see what was generated by Yolk. Run the following command to start the server:

Windows Command Line Shell
	deno run --allow-net --allow-read --allow-write server.js
Linux Command Line Shell
	./deno run --allow-net --allow-read --allow-write server.js

The default port used by Yolk is 55555. You can connect to the server in your browser using the following address:

Yolk Server Address
	http://localhost:55555/main

You'll see a page like this:



Yolk, all its commands, the project structures created, and all the libraries used are well documented in the Yolk repository. You can refer to the repository for additional information.

There are some changes we will make by default to the application generated by Yolk, as Yolk includes middleware that are relevant for web servers serving multiple users, but not in the case of a single user. In particular we will remove the Session middleware and the Snelm middleware, as these features won't be needed for the application, allowing us to save some CPU cycles and RAM. However we will keep the logging middleware and the query string parser middleware, as those can be useful for our application (such as for debugging in the case of the logging middleware). Our new server.js file should look like this:

server.js
	import { Application } from "./deps/oak.js";
	import { Database } from './deps/denodb.js';
	import { dbconfig } from "./config/dbconfig.js";
	import { queryParserAsync } from "./deps/oakAsyncQueryParser.js";
	import { organ } from "./deps/organ.js";
	
	const app = new Application();
	
	
	/* Server configurations */
	let port = 55555;
	if (Deno.args[0]) {
		port = Number.parseInt(Deno.args[0]);
	}
	
	
	/* Database */
	const database = new Database(dbconfig.client, dbconfig.connection);
	app.use(async (context, next) => {
		context.database = database;
		await next();
	});
	
	
	/* Middleware */
	// Logging middleware
	app.use(organ());
	
	// Query Parser Middleware
	app.use(queryParserAsync());
	
	
	/* Routers */
	import main from "./applets/main/router.js";
	app.use(main.routes());
	app.use(main.allowedMethods());
	
	import mainapi from "./applets/main/api/router.js";
	app.use(mainapi.routes());
	app.use(mainapi.allowedMethods());
	
	
	// General 404 Error Page
	app.use(async (context, next) => {
		context.response.status = 404;
		context.response.body = "404 - Page Not Found";
	
		await next();
	});
	
	
	// Starting the server
	console.log("Starting server at port: " + port);
	await app.listen({ port: port });

Now that our server is in place, we can set some configurations for the DenoCEF application.


Modifying app.js for Use with Yolk

Now that the server is created, let's modify the app.js file to make the CEF instance interact with the server generated by Yolk. The app.js file uses the DenoCEF API to start the server and the CEF instance and set up the sandbox for the server. The app.js file contains these contents by default:

app.js
	import { Cef } from "./denocef/cef.js";

	let cefapp = new Cef({
		urlPath: "/",
	});
	
	await cefapp.run();

Note that an instance of DenoCef is created with a series of configurations and then finally the server and CEF are run using the .run() method call. The current configurations were set for the prior server.js file, but they need to be updated for the server created by Yolk. Change the configurations to the follow:

app.js
	import { Cef } from "./denocef/cef.js";

	let cefapp = new Cef({
		urlPath: "/main",
		width: 600,
		height: 350,
		x: 100,
		y: 100,
		denoPermissions: [
			"--allow-read=./",
			"--allow-write=./",
		],
	});
	
	await cefapp.run();

Here you can see a few new configurations. For starters, I've changed the urlPath for the entry point URL route for the application. This essentially tells the CEF instance to connect to http://localhost:<port>/main instead of the default http://localhost:<port>/. I also changed the denoPermissions for this application. By default, the DenoCEF API blocks any usage of the -A or --allow-all flags, and requires you to use specific flags. Since the server created by Yolk includes an embedded Sqlite3 database, I've included the --allow-read and --allow-write flags, but limited their access to only the project directory. Outside of this directory the server has no read or write permissions. For a full list of configurations for the DenoCef object, see the DenoCEF repository. Finally, I've also changed the width and height of the CEF instance window, and set its x and y positions when the new window spawns.

Now try running the DenoCEF application with the following command:

Windows (either through the command line or by double clicking the file)
	run.vbs
Linux Command Line Shell
	./run

You should see a window open up with the same Yolk starting page as we saw above:



Now that we've created our application, lets look at how to package it.


Packaging Our DenoCEF Application

Now that we have a fully fledged web server that can run with our CEF instance and that is sandboxed, we can package our application to share with others. The steps to package is fairly easy, with the general steps involving adding your deno cache to the directory and then archiving and compressing the folder (such as via a zip file or a tarball). Fortunately, to make the process easier, the Electric CLI has an option to do this process for you. You can run the following command:

Command Line Shell
	electric package

And electric will start working to move your deno cache to the directory and link the libraries to it. Additionally, it will create a new run script that will set the DENO_DIR environment variable so that it points to your local deno_modules folder.

A Warning About Packaging

Once packaging is complete, your entire application and all dependencies will be contained within your project folder. You can then zip or tarball the folder and move it to other computers. You now have a portable, packaged application using CEF and Deno!




Summary

DenoCEF, Electric, and Yolk are excellent tools for creating large-scale and complex cross platform applications. The benefit of using these tools is that you can use virtually the exact same APIs for desktop GUI development as you do for web development. Additionally, due to the excellent design choices by Ryan Dahl and the Deno team, your application can be well sandboxed, providing users a level of comfort and understanding of the internals of the application.

While DenoCEF does provide good benefits for making desktop GUI applications, it does not invalidate the use of Deno Webview for making desktop GUI applications. Since DenoCEF includes a full browser within your application, the resulting package size will be quite large compared to Deno Webview. Below are the use cases for each of the two libraries:


Use DenoCEF when:

Use Deno Webview when:


If you have any comments or questions on this article, please feel free to reach out to me at denotutorials@protonmail.com, or to reach out through the DenoCEF github repository.