5.10 Load module

This subsection holds significant importance, perhaps being the most pivotal one within the text. In the upcoming paragraphs, as well as those that follow, the discussion revolves around the manner in which Deno undertakes the loading of modules into the V8 engine. Up until this juncture, Deno's efforts have predominantly been directed towards our specific program by transforming the given path into a module specifier and accomplishing a substantial portion of its initialization tasks.

Overview

JsRuntime's load_main_module functions as a substantial piece of code, not necessarily due to its sheer length, but rather due to its multifaceted functionality. In the upcoming discussion, we will delve into key components of this function. The process of loading a module follows a recursive pattern. This loading process concludes only when both the main module and its subordinate dependencies have been effectively loaded.
The sequence of actions involved in module loading occurs through a sequence of distinct steps:
We have repeatedly traversed these steps multiple times within the recent preceding sections. We begin by considering the root module and initiating its loading. Following this, we load all associated dependencies. Afterward, both the root module and its dependencies are brought into existence. This phase involves the instantiation of the root module along with its dependent modules. As we conclude this loading process, the main module, along with its dependencies, finds instantiation within the V8 engine. This iterative process has been extensively covered in the preceding sections, providing a comprehensive understanding of the sequential actions involved.

Steps in loading

The process of loading a module in Deno occurs through a sequence of clear steps. Let's take a closer look at each of these steps to better understand how it all works:
  1. 1.
    Acquiring the Module Loader: To begin, Deno retrieves the module loader, which is responsible for managing and orchestrating the module loading process.
  2. 2.
    Loading and Registering Modules and Dependencies: The next step involves a recursive procedure where both the main module and all its associated dependencies are loaded and registered. Deno meticulously fetches each module, making sure all the interdependencies are accounted for and properly integrated.
  3. 3.
    Generating the Module Graph: As a result of the comprehensive recursive loading process, something referred to as a "module graph" is formed. This graph essentially represents the intricate web of connections between different modules and their relationships.
  4. 4.
    Creating Instances of the Main Module: After successfully establishing the module graph, Deno proceeds to create instances of the main module. This is achieved using the unique identifier of the root (main) module as a reference.
The underlying code that drives the loading of the main module within the JavaScript runtime is encapsulated in the function named load_main_module. This function embodies the logic that orchestrates the entire process we've just outlined.
pub async fn load_main_module(
&self,
isolate: &mut v8::Isolate,
specifier: &ModuleSpecifier,
code: Option<ModuleCode>,
) -> Result<ModuleId, Error> {
let module_map_rc = self.0.module_map();
if let Some(code) = code {
let specifier = specifier.as_str().to_owned().into();
let scope = &mut self.handle_scope(isolate);
// true for main module
module_map_rc
.borrow_mut()
.new_es_module(scope, true, specifier, code, false)
.map_err(|e| match e {
ModuleError::Exception(exception) => {
let exception = v8::Local::new(scope, exception);
exception_to_err_result::<()>(scope, exception, false).unwrap_err()
}
ModuleError::Other(error) => error,
})?;
}
let mut load =
ModuleMap::load_main(module_map_rc.clone(), &specifier).await?;
while let Some(load_result) = load.next().await {
let (request, info) = load_result?;
let scope = &mut self.handle_scope(isolate);
load.register_and_recurse(scope, &request, info).map_err(
|e| match e {
ModuleError::Exception(exception) => {
let exception = v8::Local::new(scope, exception);
exception_to_err_result::<()>(scope, exception, false).unwrap_err()
}
ModuleError::Other(error) => error,
},
)?;
}
let root_id = load.root_module_id.expect("Root module should be loaded");
self.instantiate_module(isolate, root_id).map_err(|e| {
let scope = &mut self.handle_scope(isolate);
let exception = v8::Local::new(scope, e);
exception_to_err_result::<()>(scope, exception, false).unwrap_err()
})?;
Ok(root_id)
}
Now, let's take a quick look at the steps involved in this process.

Recursively load module

Recursive module loading might seem intricate, but it's a systematic process that unfolds in the following manner:
We kick off with the root module or the main module, treating it as the starting point. This module is fetched from its source. Afterward, it's established as the foundational module within the module graph, which is like a visual representation of how different modules are connected.
To make sense of the main module's content, it's parsed – essentially, we break down its code to understand its structure and components. But a module rarely stands alone; it often relies on other modules. Thus, we traverse through its direct dependencies, each time fetching a dependency, parsing it, and then adding it to our module graph.
This recursive procedure keeps going until all the required dependencies have been processed. As the code in these modules contains imports, these imports signify ES modules, a way of structuring code for better organization and reusability.
In this journey of exploration, the code dives deeper into the module tree, loading each module it encounters along the way. Having constructed the graph step by step, the next phase involves transpilation – converting the graph into a format that's more suitable for execution.
Now, it's time to traverse this transpiled graph, registering each module as we encounter them. The purpose of registration is to furnish all the necessary module details to the V8 engine, which is responsible for executing JavaScript code.
Once everything is set up, modules come to life. The process of instantiation involves taking each module and its dependencies and initializing them within the V8 engine. This final step ensures that your code can run seamlessly, leveraging the interconnected modules to perform the tasks you've programmed.
In essence, recursive module loading is like a journey through a forest of interconnected trees, where each tree represents a module, and the steps we take ensure that we explore, understand, and assemble these modules effectively for smooth execution.
That was quite a handful of steps! The process of module loading can be quite intricate. We will delve into its intricacies as we progress further. But when observed from the perspective of the v8 engine, module loading essentially boils down to only two steps:
  • Loading the module
  • Instantiating it

Register modules

Iterate through the graph in an asynchronous manner while registering all the modules. This process is notably simpler when contrasted with loading modules recursively. By adopting this approach, the complexity of handling module registrations is significantly reduced. In comparison to the intricate nature of recursive module loading, this method offers a straightforward and efficient means of managing module dependencies.

Instantiate the main module

Upon the completion of the registration process, both the main module and its dependencies are loaded into the V8 engine. Now comes the moment to create an instance of the root module. This action is only necessary for the root module; V8 will take care of managing and retrieving all the required dependencies using callbacks.
--
That was just a brief introduction to get us started. Now, let's delve into the next section, where we'll gain a deeper understanding of how the module graph is constructed. This module graph holds significant importance and serves as a fundamental data structure within Deno's workings.