Repository: denoland/deno_core
Pull request #366 (2023-12-05 00:26:53)

feat: add ext:core/mod.js
This commit adds "ext:core/mod.js" built-in ES module that reexports
"core", "internals" and "primordials" properties of the 
"globalThis.__bootstrap" namespace. This is very convenient for
embedders that author runtime code using ES modules instead of
scripts, because it allows to import these props directly instead of
capturing "globalThis.__bootstrap" namespace.

To achieve that a new "ModuleMap::lazy_load_es_module_from_code"
method was added that accepts a specifier and source code; instantiates
and evaluates the provided code as ES module.

This will be very useful for https://github.com/denoland/deno_core/pull/263
and https://github.com/denoland/deno/pull/21422.

bartlomieju: Probably worth to run some benchmarks to see startup time difference, but I expect it'll be <0.1ms.

Patch:
diff --git a/core/mod.js b/core/mod.js
new file mode 100644
index 0000000..a7221f9
--- /dev/null
+++ b/core/mod.js
@@ -0,0 +1,6 @@
+// Re-export fields from `globalThis.__bootstrap` so that embedders using
+// ES modules can import these symbols instead of capturing the bootstrap ns.
+const bootstrap = globalThis.__bootstrap;
+const { core, internals, primordials } = bootstrap;
+
+export { core, internals, primordials };
diff --git a/core/modules/map.rs b/core/modules/map.rs
index 59d7bcb..45b33cc 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -23,6 +23,7 @@ use crate::runtime::JsRuntimeState;
 use crate::runtime::SnapshottedData;
 use crate::JsRuntime;
 use crate::ModuleSource;
+use crate::ModuleSpecifier;
 use anyhow::bail;
 use anyhow::Error;
 use futures::future::FutureExt;
@@ -1301,6 +1302,58 @@ impl ModuleMap {
 
     vec![]
   }
+
+  /// Load and evaluate an ES module provided the specifier and source code.
+  ///
+  /// The module should not have Top-Level Await (that is, it should be
+  /// possible to evaluate it synchronously).
+  ///
+  /// It is caller's responsibility to ensure that not duplicate specifiers are
+  /// passed to this method.
+  pub(crate) fn lazy_load_es_module_from_code(
+    &self,
+    scope: &mut v8::HandleScope,
+    module_specifier: &str,
+    source_code: ModuleCode,
+  ) -> Result<v8::Global<v8::Value>, Error> {
+    let specifier = ModuleSpecifier::parse(module_specifier)?;
+    let mod_id = self
+      .new_es_module(scope, false, specifier.into(), source_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,
+      })?;
+
+    self.instantiate_module(scope, mod_id).map_err(|e| {
+      let exception = v8::Local::new(scope, e);
+      exception_to_err_result::<()>(scope, exception, false).unwrap_err()
+    })?;
+
+    let module_handle = self.get_handle(mod_id).unwrap();
+    let module_local = v8::Local::<v8::Module>::new(scope, module_handle);
+
+    let status = module_local.get_status();
+    assert_eq!(status, v8::ModuleStatus::Instantiated);
+
+    let value = module_local.evaluate(scope).unwrap();
+    let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
+    let result = promise.result(scope);
+    if !result.is_undefined() {
+      return Err(
+        exception_to_err_result::<()>(scope, result, false).unwrap_err(),
+      );
+    }
+
+    let status = module_local.get_status();
+    assert_eq!(status, v8::ModuleStatus::Evaluated);
+
+    let mod_ns = module_local.get_module_namespace();
+
+    Ok(v8::Global::new(scope, mod_ns))
+  }
 }
 
 impl Default for ModuleMap {
diff --git a/core/modules/module_map_data.rs b/core/modules/module_map_data.rs
index 96356a8..5258de0 100644
--- a/core/modules/module_map_data.rs
+++ b/core/modules/module_map_data.rs
@@ -491,10 +491,11 @@ impl ModuleMapData {
   #[cfg(test)]
   pub fn assert_module_map(&self, modules: &Vec<ModuleInfo>) {
     let data = self;
-    assert_eq!(data.handles.len(), modules.len());
-    assert_eq!(data.info.len(), modules.len());
+    // There's always one internal `deno_core` ES module loaded, so +1 here.
+    assert_eq!(data.handles.len(), modules.len() + 1);
+    assert_eq!(data.info.len(), modules.len() + 1);
     assert_eq!(data.next_load_id as usize, modules.len());
-    assert_eq!(data.by_name.len(), modules.len());
+    assert_eq!(data.by_name.len(), modules.len() + 1);
 
     for info in modules {
       assert!(data.handles.get(info.id).is_some());
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index df04605..007e841 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -221,6 +221,8 @@ pub(crate) const BUILTIN_SOURCES: [ExtensionFileSource; 3] = include_js_files!(
   "01_core.js",
   "02_error.js",
 );
+pub(crate) const BUILTIN_ES_MODULES: [ExtensionFileSource; 1] =
+  include_js_files!(core "mod.js",);
 
 /// A single execution context of JavaScript. Corresponds roughly to the "Web
 /// Worker" concept in the DOM.
@@ -919,6 +921,14 @@ impl JsRuntime {
             file_source.load()?,
           )?;
         }
+        for file_source in &BUILTIN_ES_MODULES {
+          let mut scope = realm.handle_scope(self.v8_isolate());
+          module_map.lazy_load_es_module_from_code(
+            &mut scope,
+            file_source.specifier,
+            file_source.load()?,
+          )?;
+        }
       }
       self.init_cbs(&realm);
 
diff --git a/core/runtime/snapshot_util.rs b/core/runtime/snapshot_util.rs
index 76dcdaa..ca0ff45 100644
--- a/core/runtime/snapshot_util.rs
+++ b/core/runtime/snapshot_util.rs
@@ -4,6 +4,7 @@ use std::path::Path;
 use std::path::PathBuf;
 use std::time::Instant;
 
+use crate::runtime::jsruntime::BUILTIN_ES_MODULES;
 use crate::runtime::jsruntime::BUILTIN_SOURCES;
 use crate::Extension;
 use crate::ExtensionFileSourceCode;
@@ -57,6 +58,13 @@ pub fn create_snapshot(
       files_loaded_during_snapshot.push(PathBuf::from(path));
     }
   }
+  for source in &BUILTIN_ES_MODULES {
+    if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) =
+      &source.code
+    {
+      files_loaded_during_snapshot.push(PathBuf::from(path));
+    }
+  }
   for source in js_runtime
     .extensions()
     .iter()


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index 63a2025..4499e78 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -1380,3 +1380,32 @@ async fn import_meta_resolve_cb() {
   a.await.unwrap().unwrap();
   b.await.unwrap().unwrap();
 }
+
+#[test]
+fn builtin_core_module() {
+  let main_specifier = resolve_url("file:///main_module.js").unwrap();
+
+  let source_code =
+    r#"import { core, primordials, internals } from "ext:core/mod.js";
+if (typeof core === "undefined") throw new Error("core missing");
+if (typeof primordials === "undefined") throw new Error("core missing");
+if (typeof internals === "undefined") throw new Error("core missing");
+"#
+    .to_string();
+  let loader =
+    StaticModuleLoader::new([(main_specifier.clone(), source_code.into())]);
+
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(Rc::new(loader)),
+    ..Default::default()
+  });
+
+  let main_id_fut = runtime
+    .load_main_module(&main_specifier, None)
+    .boxed_local();
+  let main_id = futures::executor::block_on(main_id_fut).unwrap();
+
+  #[allow(clippy::let_underscore_future)]
+  let _ = runtime.mod_evaluate(main_id);
+  futures::executor::block_on(runtime.run_event_loop(false)).unwrap();
+}
diff --git a/core/runtime/tests/snapshot.rs b/core/runtime/tests/snapshot.rs
index fbf9b0d..d8dc46f 100644
--- a/core/runtime/tests/snapshot.rs
+++ b/core/runtime/tests/snapshot.rs
@@ -147,7 +147,8 @@ fn es_snapshot() {
       )
       .unwrap()
     };
-    assert_eq!(i, id);
+    // There's always one internal `deno_core` ES module loaded, so +1 here.
+    assert_eq!(i + 1, id);
 
     #[allow(clippy::let_underscore_future)]
     let _ = runtime.mod_evaluate(id);