5.3 Main program of Deno

Deno command

We'll use the following command to run our code in Deno:
deno run helloLog.ts
  • deno is the name of the executable
  • run is the subcommand
  • helloLog.ts is the program to run
This simple program doesn't need any permissions, but we'll still take a look at permissions in this chapter as permissions are one of the foundations of Deno. They help to sandbox the runtime.

Main program

Deno is written in Rust, so the main program also resides in the Rust code. The main program of Deno is present inside CLI, which is both the orchestrator and service provider.
Here is the main program of Deno:
pub fn main() {
#[cfg(windows)]
colors::enable_ansi(); // For Windows 10
let args: Vec<String> = env::args().collect();
if let Err(err) = standalone::try_run_standalone_binary(args.clone()) {
eprintln!("{}: {}", colors::red_bold("error"), err.to_string());
std::process::exit(1);
}
let flags = flags::flags_from_vec(args);
if !flags.v8_flags.is_empty() {
init_v8_flags(&*flags.v8_flags);
}
init_logger(flags.log_level);
let subcommand_future = get_subcommand(flags);
let result = tokio_util::run_basic(subcommand_future);
if let Err(err) = result {
eprintln!("{}: {}", colors::red_bold("error"), err.to_string());
std::process::exit(1);
}
}
The deno command is a toolchain and by itself can't run. Deno needs subcommands to work its magic.
The main function of Deno does the following:
  • Build flags from command line args
  • Initialize logger
  • Get a future of the subcommand
  • Run the future
Futures in Rust are like promises in Javascript. Futures finish at some time in the future. Futures are extremely useful in writing asynchronous code.

Flags

A flag is an object that parses the sub-command specific command-line arguments and stores them for ease of access. Deno has many subcommands and each subcommand has different arguments. Based on the subcommand, flags parses the relevant args and saves them.
Here is the master list of all the parsed attributes present in the flags object:
  • subcommand
  • allow_env
  • allow_hrtime
  • allow_net
  • allow_plugin
  • allow_read
  • allow_run
  • allow_write
  • cache_blocklist
  • ca_file
  • cached_only
  • config_path
  • coverage
  • ignore
  • import_map_path
  • inspect
  • inspect_brk
  • lock
  • lock_write
  • log_level
  • net_allowlist
  • no_check
  • no_prompts
  • no_remote
  • read_allowlist
  • reload
  • repl
  • seed
  • unstable
  • v8_flags
  • version
  • watch
  • write_allowlist
Some of the attributes are boolean like allow_read, allow_write, allow_env, etc. Some of the attributes are lists like write_allowlist, read_allowlist, etc.
Deno has a long list of subcommands, however, the focus of this book is on running code. So, we'll go deep into run command only.

Run command

The purpose of the run command is to run the program. The implementation of the run command looks very simple, but it does a lot in the back. First, let's take a look at the source of the run command.
async fn run_command(flags: Flags, script: String) -> Result<(), AnyError> {
// Read script content from stdin
if script == "-" {
return run_from_stdin(flags).await;
}
if flags.watch {
return run_with_watch(flags, script).await;
}
let main_module = ModuleSpecifier::resolve_url_or_path(&script)?;
let program_state = ProgramState::new(flags.clone())?;
let permissions = Permissions::from_options(&flags.clone().into());
let mut worker =
create_main_worker(&program_state, main_module.clone(), permissions);
debug!("main_module {}", main_module);
worker.execute_module(&main_module).await?;
worker.execute("window.dispatchEvent(new Event('load'))")?;
worker.run_event_loop().await?;
worker.execute("window.dispatchEvent(new Event('unload'))")?;
Ok(())
}
That's it! This is all the code that's present in the implementation of the run command. Surely it looks very simple. However, there is a lot happening in the functions that get called from run_command.
Let's go over line by line and get an overview of all the important functions. We'll go into detail in subsequent sections. We'll skip the first two ifs present in the above function. These ifs aren't related in this context.

ModuleSpecifier

Deno converts the input script path to URL format. The URL format could be either file:// or http:// or https://. Even for local files, it uses URL format which is file://. This is to maintain consistency in the handling of paths.

ProgramState

Creates a new program state which in turn consists of:
  • flags
  • Deno's working directory
    • Deno needs a separate working directory where it stores the files
  • file fetcher
  • Typescript compiler
  • etc.

create_main_worker

Create a new main worker. Note that the main worker runs in the main thread, not a new thread. Creating the main worker also creates a new JS runtime.

worker - execute_module

Fetch, load, and evaluate the main module (we'll see this in detail very soon).

worker - execute load event

At this point, the main module is loaded and evaluated, so send a 'load' event to all the listeners.

worker - run_event_loop

Runs the event loop till the program finishes.

worker - execute unload event

The event loop has completed now, which means that the program has ended. So, send an 'unload' event to all the listeners
--
This was only a very high-level description of what is happening inside the run command. Let's go over all the steps in detail. We'll start with the concept of ModuleSpecifier.