5.4 Module Specifier

The initial task for executing a program involves crafting a module specifier designated for the main module. In Deno, the main module serves as the initial piece of code, file, or program that is provided as input. This core module is also referred to as the root module or the main module.
let main_module = cli_options.resolve_main_module()?;

Overview

The concept of a "Module Specifier" permeates throughout Deno's codebase. Essentially, the role of a Module Specifier in Deno is to transform an input path into a URL format. This format can take on various forms, such as "file," "http," or "https" URLs. This URL format is crucial because it helps Deno accurately locate and access the required modules, whether they are local files or resources from the web.
The code to resolve the main module is as follows:
DenoSubcommand::Run(run_flags) => {
if run_flags.is_stdin() {
std::env::current_dir()
.context("Unable to get CWD")
.and_then(|cwd| {
resolve_url_or_path("./$deno$stdin.ts", &cwd)
.map_err(AnyError::from)
})
} else if run_flags.watch.is_some() {
resolve_url_or_path(&run_flags.script, self.initial_cwd())
.map_err(AnyError::from)
} else if NpmPackageReqReference::from_str(&run_flags.script).is_ok() {
ModuleSpecifier::parse(&run_flags.script).map_err(AnyError::from)
} else {
resolve_url_or_path(&run_flags.script, self.initial_cwd())
.map_err(AnyError::from)
}
}
The main interface is called "resolve_url_or_path," and it's also utilized by various other subcommands. This interface combines two distinct functionalities: resolving URLs and resolving file paths. These functionalities are designed to take the input, which could be either a URL or a file path, and convert it into a format that adheres to a Uniform Resource Identifier (URI) scheme. This applies to different types of files, whether they are stored locally or accessed remotely. As a result of using this interface, you obtain an object known as the "ModuleSpecifier," which encapsulates the resolved information about the input.

Functionality

As we discussed previously, the primary API called resolve_url_or_path carries out two main functions:
  • resolve_url
  • resolve_path
Just as the names imply, the initial function is used for resolving a URL, whereas the latter one is employed to resolve a local file path. This means that the first function helps in figuring out the details of a web address, while the second function assists in determining the specifics of a file's location on your device.

resolve_url_or_path

Here is the source of the function resolve_url_or_path:
pub fn resolve_url_or_path(
specifier: &str,
current_dir: &Path,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
if specifier_has_uri_scheme(specifier) {
resolve_url(specifier)
} else {
resolve_path(specifier, current_dir)
}
}
The code is quite straightforward.
  • If input already has a URI scheme
    • resolve as URL
  • Otherwise,
    • resolve as a path (for local files)
Let's take an example to understand this better.
> deno run helloLog.ts
// RESOLVED PATH --
file:///Users/mayankc/Work/source/deno-vs-nodejs/helloLog.ts
// --
> deno run https://raw.githubusercontent.com/mayankchoubey/deno-vs-nodejs/master/helloLog.ts
// RESOLVED PATH --
https://raw.githubusercontent.com/mayankchoubey/deno-vs-nodejs/master/helloLog.ts
The source code for the resolve_url function is also straightforward. It's not overly complex and can be easily understood.
pub fn resolve_url(
url_str: &str,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
Url::parse(url_str).map_err(ModuleResolutionError::InvalidUrl)
}
Since the input is already in the form of a URL, we proceed to parse the URL mainly to validate its correctness. Now, let's delve into the source of the resolve_path function, which is notably straightforward:
pub fn resolve_path(
path_str: &str,
current_dir: &Path,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
let path = current_dir.join(path_str);
let path = normalize_path(path);
Url::from_file_path(&path)
.map_err(|()| ModuleResolutionError::InvalidPath(path))
}
In resolving the path, it takes three steps:
  • Append current directory path to input path
    • Example: /Users/mayankc/Work/source/denoExamples/ + helloLog.ts
  • The path is normalized
    • Example: /Users/mayankc/Work/source/denoExamples/ + ../../ABC.ts gets normalized to /Users/mayankc/Work/ABC.ts
  • Build a file URI scheme from the path
    • Example: /Users/mayankc/Work/source/denoExamples/helloLog.ts gets converted to file:///Users/mayankc/Work/source/denoExamples/helloLog.ts

resolve_import

Up until now, we have examined how Deno handles the determination of the main module. This module refers to the primary file specified when using the 'deno run' command. But what about imports? It's important to note that imports also undergo a comparable resolution process. While we're discussing this subject, let's delve into the concept of import resolution. It's worth mentioning that we will explore imports more extensively in Chapter 6 of this book. This will provide us with a deeper understanding of how Deno manages and resolves imported modules.
pub fn resolve_import(
specifier: &str,
base: &str,
) -> Result<ModuleSpecifier, ModuleResolutionError> {
let url = match Url::parse(specifier) {
// 1. Apply the URL parser to specifier.
// If the result is not failure, return he result.
Ok(url) => url,
// 2. If specifier does not start with the character U+002F SOLIDUS (/),
// the two-character sequence U+002E FULL STOP, U+002F SOLIDUS (./),
// or the three-character sequence U+002E FULL STOP, U+002E FULL STOP,
// U+002F SOLIDUS (../), return failure.
Err(ParseError::RelativeUrlWithoutBase)
if !(specifier.starts_with('/')
|| specifier.starts_with("./")
|| specifier.starts_with("../")) =>
{
let maybe_referrer = if base.is_empty() {
None
} else {
Some(base.to_string())
};
return Err(ImportPrefixMissing(specifier.to_string(), maybe_referrer));
}
// 3. Return the result of applying the URL parser to specifier with base
// URL as the base URL.
Err(ParseError::RelativeUrlWithoutBase) => {
let base = Url::parse(base).map_err(InvalidBaseUrl)?;
base.join(specifier).map_err(InvalidUrl)?
}
// If parsing the specifier as a URL failed for a different reason than
// it being relative, always return the original error. We don't want to
// return `ImportPrefixMissing` or `InvalidBaseUrl` if the real
// problem lies somewhere else.
Err(err) => return Err(InvalidUrl(err)),
};
Ok(url)
}