rsvim_core 0.1.3-alpha.2

The core library for RSVIM text editor.
Documentation
//! Module map and module graph.
//!
//! # Terms
//!
//! - Module graph: it maintains the relationships between a module and its
//!   dependencies tree.
//! - Module map: it maintains all the dependencies loaded into the js runtime,
//!   i.e. the rsvim editor.
//!
//! # Module
//!
//! In the most popular js runtime [node.js](https://nodejs.org), a module is loaded by the keyword
//! `require` (Common JS) or `import` (ECMAScript Module). And there're two kinds of imports:
//! static and dynamic. For example:
//!
//! Case-1: Static Import
//!
//! ```javascript
//! // ES Module
//! const _ = import "lodash";
//!
//! // Common JS
//! const _ = require("lodash");
//! ```
//!
//! // Case-2: Dynamic Import
//!
//! ```javascript
//! // Wait for import complete.
//! const _ = await import("lodash");
//!
//! // Trigger callbacks on import complete.
//! import("lodash")
//!   .then((_) => {})
//!   .catch((err) => {});
//! ```
//!
//! Static import runs synchronizely, dynamic import runs asynchronizely.

use crate::js::module::ModulePath;
use crate::js::module::ModuleStatus;
use crate::js::module::es_module::*;
use crate::prelude::*;
use std::collections::hash_map::Entry;
use std::fmt::Debug;

#[derive(Clone)]
/// Import kind.
pub enum ImportKind {
  // Loading static imports.
  Static,
  // Loading a dynamic import.
  Dynamic(v8::Global<v8::PromiseResolver>),
}

/// Module graph.
pub struct ModuleGraph {
  kind: ImportKind,
  root_rc: EsModuleRc,
  same_origin: Vec<v8::Global<v8::PromiseResolver>>,
}

rc_refcell_ptr!(ModuleGraph);

impl Debug for ModuleGraph {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("ModuleGraph")
      .field(
        "kind",
        match self.kind {
          ImportKind::Static => &"Static",
          ImportKind::Dynamic(_) => &"Dynamic",
        },
      )
      .field("root_rc", &self.root_rc)
      .field(
        "same_origin",
        &format!("Vec<v8::PromiseResolver>({})", self.same_origin.len()),
      )
      .finish()
  }
}

impl ModuleGraph {
  pub fn kind(&self) -> &ImportKind {
    &self.kind
  }

  pub fn root_rc(&self) -> EsModuleRc {
    self.root_rc.clone()
  }

  pub fn same_origin(&self) -> &Vec<v8::Global<v8::PromiseResolver>> {
    &self.same_origin
  }

  pub fn same_origin_mut(
    &mut self,
  ) -> &mut Vec<v8::Global<v8::PromiseResolver>> {
    &mut self.same_origin
  }
}

impl ModuleGraph {
  // Initializes a new graph resolving a static import.
  pub fn static_import(path: &str) -> ModuleGraph {
    // Create an ES module instance.
    let module = EsModule::to_rc(EsModule::new(
      path.into(),
      ModuleStatus::Fetching,
      vec![],
      None,
      false,
    ));

    Self {
      kind: ImportKind::Static,
      root_rc: module,
      same_origin: vec![],
    }
  }

  // Initializes a new graph resolving a dynamic import.
  pub fn dynamic_import(
    path: &str,
    promise: v8::Global<v8::PromiseResolver>,
  ) -> ModuleGraph {
    // Create an ES module instance.
    let module = EsModule::to_rc(EsModule::new(
      path.into(),
      ModuleStatus::Fetching,
      vec![],
      None,
      true,
    ));

    Self {
      kind: ImportKind::Dynamic(promise),
      root_rc: module,
      same_origin: vec![],
    }
  }
}

/// Module map.
/// It maintains all the modules inside js runtime, including already resolved and pending
/// fetching.
pub struct ModuleMap {
  // Entry point of runtime execution, this is the `rsvim.{js,ts}`
  // configuration entry point for Rsvim.
  main: Option<ModulePath>,

  // Maps from "Module Path" to "v8 Module".
  by_path: FoldMap<ModulePath, v8::Global<v8::Module>>,

  // Maps from "Module ID" to "v8 Module" and its path.
  by_id: FoldMap<i32, Vec<(ModulePath, v8::Global<v8::Module>)>>,

  // Module status.
  pub seen: FoldMap<ModulePath, ModuleStatus>,

  // Pending modules.
  pub pending: Vec<ModuleGraphRc>,
}

impl Debug for ModuleMap {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    f.debug_struct("ModuleMap")
      .field("main", &self.main)
      .field(
        "by_path",
        &self
          .by_path
          .keys()
          .map(|k| (k.clone(), "v8::Module".to_string()))
          .collect::<FoldMap<String, String>>(),
      )
      .field(
        "by_id",
        &self
          .by_id
          .iter()
          .map(|(k, v)| (*k, v.iter().map(|e| e.0.clone()).collect()))
          .collect::<FoldMap<i32, Vec<String>>>(),
      )
      .field("seen", &self.seen)
      .field("pending", &self.pending)
      .finish()
  }
}

impl ModuleMap {
  /// Creates a global module map.
  pub fn new() -> ModuleMap {
    Self {
      main: None,
      by_path: FoldMap::new(),
      by_id: FoldMap::new(),
      seen: FoldMap::new(),
      pending: vec![],
    }
  }

  pub fn main(&self) -> &Option<ModulePath> {
    &self.main
  }

  /// Add a compiled v8 module to the cache.
  pub fn insert(
    &mut self,
    path: &str,
    module_id: i32,
    module: v8::Global<v8::Module>,
  ) {
    // No main module has been set, so let's update the value.
    if self.main.is_none() && std::fs::metadata(path).is_ok() {
      self.main = Some(path.into());
    }
    self.by_path.insert(path.into(), module.clone());
    let entry = (path.into(), module.clone());
    match self.by_id.entry(module_id) {
      Entry::Vacant(value) => {
        value.insert(vec![entry]);
      }
      Entry::Occupied(mut value) => {
        value.get_mut().push(entry);
      }
    }
  }

  // // Returns if there are still pending imports to be loaded.
  // pub fn has_pending_imports(&self) -> bool {
  //   !self.pending.is_empty()
  // }

  /// Get v8 module by its path.
  pub fn get(&self, path: &str) -> Option<v8::Global<v8::Module>> {
    self.by_path.get(path).cloned()
  }

  /// Whether a v8 module already resolved.
  pub fn contains(&self, path: &str) -> bool {
    self.by_path.contains_key(path)
  }

  #[cfg(test)]
  /// Whether a v8 module already resolved, compare by key suffix.
  pub fn get_by_suffix(
    &self,
    path: &str,
  ) -> Option<(&ModulePath, &v8::Global<v8::Module>)> {
    self.by_path.iter().find(|(k, _v)| {
      let kp = Path::new(k).canonicalize().unwrap();
      let pp = Path::new(path).canonicalize().unwrap();
      kp.ends_with(&pp) || pp.ends_with(kp)
    })
  }

  /// Get module path by its ID.
  ///
  /// NOTE: v8 Module does not guarantee its ID is unique, thus here we use
  /// `Vec<v8::Module>` to store all the modules with the same ID, to solve the
  /// conflicted hash IDs.
  pub fn get_path(
    &self,
    module_id: i32,
    module: v8::Global<v8::Module>,
  ) -> Option<ModulePath> {
    match self.by_id.get(&module_id) {
      None => None,
      Some(entries) => {
        if entries.len() <= 1 {
          debug_assert!(!entries.is_empty());
          Some(entries[0].0.clone())
        } else {
          entries
            .iter()
            .find(|(_, m)| *m == module)
            .map(|(p, _)| p.clone())
        }
      }
    }
  }
}