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

In this subsection, we'll start by exploring a straightforward program that doesn't require any special permissions. However, even though this program doesn't demand permissions, we'll delve into the concept of permissions within this chapter. Permissions serve as one of the fundamental building blocks of Deno. They play a crucial role in creating a protective barrier around the runtime environment, ensuring security.

It's worth noting that the 'deno run' command has a specific purpose: it handles the transpilation process. Essentially, it converts TypeScript code into JavaScript, ready for execution. Interestingly, it prioritizes quick startup over conducting type checking. This means that while 'deno run' prepares the code for running, it doesn't extensively verify the types involved.

For a more stringent type checking of your application's code, Deno offers a distinct command named 'deno check'. This command serves the purpose of rigorously examining the types within your codebase. It's like a meticulous inspector, ensuring that the types align correctly and your code is as error-free as possible. This additional step can be particularly helpful in catching potential issues before they can cause problems during execution.

Deno's main program

Deno is crafted using the Rust programming language, and as a result, its primary codebase is also composed in Rust. The central component of Deno, known as the main program, is housed within the CLI. This CLI serves a dual role, functioning as both the conductor that coordinates various tasks and the supplier of essential services that Deno offers.

Contained within this CLI is the heart of Deno, its main program. This program acts as the control center, governing the execution of Deno's functionalities and managing how it interacts with your commands and scripts. It's responsible for overseeing tasks such as module loading, security checks, and runtime environment management.

pub fn main() {
  setup_panic_hook();

  util::unix::raise_fd_limit();
  util::windows::ensure_stdio_open();
  #[cfg(windows)]
  colors::enable_ansi(); // For Windows 10
  deno_runtime::permissions::set_prompt_callbacks(
    Box::new(util::draw_thread::DrawThread::hide),
    Box::new(util::draw_thread::DrawThread::show),
  );

  let args: Vec<String> = env::args().collect();

  let future = async move {
    let current_exe_path = current_exe()?;
    let standalone_res =
      match standalone::extract_standalone(&current_exe_path, args.clone())
        .await
      {
        Ok(Some((metadata, eszip))) => standalone::run(eszip, metadata).await,
        Ok(None) => Ok(()),
        Err(err) => Err(err),
      };
    // TODO(bartlomieju): doesn't handle exit code set by the runtime properly
    unwrap_or_exit(standalone_res);

    let flags = match flags_from_vec(args) {
      Ok(flags) => flags,
      Err(err @ clap::Error { .. })
        if err.kind() == clap::error::ErrorKind::DisplayHelp
          || err.kind() == clap::error::ErrorKind::DisplayVersion =>
      {
        err.print().unwrap();
        std::process::exit(0);
      }
      Err(err) => unwrap_or_exit(Err(AnyError::from(err))),
    };

    let default_v8_flags = match flags.subcommand {
      DenoSubcommand::Lsp => vec!["--max-old-space-size=3072".to_string()],
      _ => vec![],
    };
    init_v8_flags(&default_v8_flags, &flags.v8_flags, get_v8_flags_from_env());

    util::logger::init(flags.log_level);

    run_subcommand(flags).await
  };

  let exit_code =
    unwrap_or_exit(create_and_run_current_thread_with_maybe_metrics(future));

  std::process::exit(exit_code);
}

The deno command functions as a toolchain, but it cannot operate on its own to execute tasks. Deno relies on subcommands to perform its various functions and capabilities.

Regarding the main function code of Deno, it undertakes the following tasks:

  • Build flags from command line args

  • Initialize logger

  • Run the subcommand asynchronously (notice the await at the end)

Drawing a parallel between Rust's futures and JavaScript's promises can provide a clearer understanding of their roles in asynchronous programming. In the Rust programming language, futures operate similarly to promises in JavaScript. Just as promises represent values that may not be available immediately but will be fulfilled at some point, Rust futures encapsulate computations that will be completed in the future. These futures serve as powerful tools for handling asynchronous tasks, making them a cornerstone of writing efficient and responsive code.

Flags

A flag acts like a tool that examines and understands command-line details related to specific sub-commands. Its purpose is to make these details easily accessible. Deno comes with a variety of sub-commands, and each of these sub-commands comes with its own set of arguments. To simplify things, flags are used to sort through these arguments based on the specific sub-command being used. This way, the important arguments are picked out and stored for further use, all depending on which sub-command you're working with. In essence, flags help organize the command-line information so that Deno can effectively handle the various sub-commands and their unique requirements.

Each sub-command in Deno comes with its own set of flags that are tailored to its specific functions. Let's delve into a few frequently employed sub-commands to gain a better understanding.

pub struct CheckFlags {
  pub files: Vec<String>,
}

pub struct CoverageFlags {
  pub files: FileFlags,
  pub output: Option<PathBuf>,
  pub include: Vec<String>,
  pub exclude: Vec<String>,
  pub lcov: bool,
}

pub struct RunFlags {
  pub script: String,
  pub watch: Option<WatchFlagsWithPaths>,
}

In Deno, there are certain attributes that are in the form of booleans, such as allow_read, allow_write, and allow_env. These attributes can be thought of as settings that determine whether specific actions are permitted or not. For instance, allow_read controls whether the script is allowed to read files or not, allow_write governs the ability to write to files, and allow_env manages access to environment variables. By setting these attributes to either "true" or "false," you can specify the permissions granted to the script.

Additionally, Deno provides attributes that are in the form of lists, such as write_allowlist and read_allowlist. These lists contain specific paths or resources that are explicitly permitted. When you include paths in the write_allowlist, the script is only allowed to write to the specified paths, and similarly, when you include paths in the read_allowlist, the script can only read from those specified paths. This mechanism allows for a more granular control over the file system and resource access, enhancing security and minimizing unintended interactions.

Below is a compilation of frequently utilized command line options in Deno. These options allow you to interact with the Deno runtime effectively:

  • --allow-all

  • --allow-read

  • --deny-read

  • --allow-write

  • --deny-write

  • --allow-net

  • --deny-net

  • --unsafely-ignore-certificate-errors

  • --allow-env

  • --deny-env

  • --allow-run

  • --deny-run

  • --allow-sys

  • --deny-sys

  • --allow-ffi

  • --deny-ffi

  • --allow-hrtime

  • --deny-hrtime

Deno offers an extensive array of subcommands, yet the primary focus of this book revolves around the execution of code. Therefore, our in-depth exploration will be solely dedicated to the 'run' command. While Deno presents a wide range of subcommands catering to various functionalities, our attention within this book remains centered on the intricacies of the 'run' command's utilization.

Run command

The run command in Deno serves the purpose of executing your program. Although the implementation of this command may appear straightforward, it actually undertakes numerous tasks behind the scenes. Let's delve into the mechanics of the run command and explore its inner workings.

pub async fn run_script(
  flags: Flags,
  run_flags: RunFlags,
) -> Result<i32, AnyError> {
  if !flags.has_permission() && flags.has_permission_in_argv() {
    log::warn!(
      "{}",
      crate::colors::yellow(
        r#"Permission flags have likely been incorrectly set after the script argument.
To grant permissions, set them before the script argument. For example:
    deno run --allow-read=. main.js"#
      )
    );
  }

  if let Some(watch_flags) = run_flags.watch {
    return run_with_watch(flags, watch_flags).await;
  }

  // TODO(bartlomieju): actually I think it will also fail if there's an import
  // map specified and bare specifier is used on the command line
  let factory = CliFactory::from_flags(flags).await?;
  let deno_dir = factory.deno_dir()?;
  let http_client = factory.http_client();
  let cli_options = factory.cli_options();

  // Run a background task that checks for available upgrades. If an earlier
  // run of this background task found a new version of Deno.
  super::upgrade::check_for_upgrades(
    http_client.clone(),
    deno_dir.upgrade_check_file_path(),
  );

  let main_module = cli_options.resolve_main_module()?;

  maybe_npm_install(&factory).await?;

  let permissions = PermissionsContainer::new(Permissions::from_options(
    &cli_options.permissions_options(),
  )?);
  let worker_factory = factory.create_cli_main_worker_factory().await?;
  let mut worker = worker_factory
    .create_main_worker(main_module, permissions)
    .await?;

  let exit_code = worker.run().await?;
  Ok(exit_code)
}

And that's all there is to it! The code you see here constitutes the entirety of the implementation for the run command. It may seem remarkably straightforward on the surface. Nevertheless, beneath this apparent simplicity, a significant amount of activity takes place within the functions that are invoked from the run_script function.

Now, let's delve into the essential aspects of the run_script function and gain an overview of its crucial functions. As we proceed, we'll explore these concepts in greater depth within the upcoming sections. Our focus for now will be on the pertinent segments of the code that deserve attention.

ModuleSpecifier

The initial file used in the application, which is provided as input to the 'deno run' command, is referred to as the main file or main module of the application. This main module serves as the entry point where the application's execution begins. When you run your Deno application, Deno looks for the specified main file and starts executing the code from there.

The primary file you use in Deno can either exist on your computer (what we call "local") or be hosted on the internet (which we refer to as "remote"). Deno is smart enough to change the script's location into a special web address called a URL. URLs can come in different flavors, like file:// or http:// or even https://. Interestingly, Deno treats local files the same way as remote ones, using the file:// format. This might seem a bit strange, but it's actually a way to make sure that Deno handles all types of files in a consistent manner.

Check for upgrades

When you execute a program in Deno, you may have observed an informational message when it starts up. This message notifies you about the availability of a new version of Deno. This happens because Deno is designed to keep you updated with its latest versions, ensuring you have access to the newest features, improvements, and security enhancements.

> deno run -A main.ts 
A new release of Deno is available: 1.35.3  1.36.1 Run `deno upgrade` to install it.
Listening on http://localhost:8000/

Main worker

The following step involves crafting a main worker factory, which is then employed to construct a main worker. The main worker is essentially the core thread that operates the primary module. This central worker is inherently generated and consistently linked with the main module. In comparison, web workers are forged when required.

After the establishment of the main worker, the program commences its execution through the employment of the main worker's run API. This API facilitates the initiation of the program's operational journey within the main worker's domain.

--

The earlier explanation provides a basic overview of the processes occurring within the run command. Now, let's delve into a comprehensive breakdown of each individual step. Our exploration will commence by understanding the fundamental concept of ModuleSpecifier.

Last updated