Skip to content

Commit

Permalink
feat: add ext:core/mod.js (denoland#366)
Browse files Browse the repository at this point in the history
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
denoland/deno_core#263
and denoland#21422.
  • Loading branch information
bartlomieju authored Dec 5, 2023
1 parent 4347ba6 commit 4caa187
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 4 deletions.
6 changes: 6 additions & 0 deletions core/mod.js
Original file line number Diff line number Diff line change
@@ -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 };
53 changes: 53 additions & 0 deletions core/modules/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions core/modules/module_map_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
29 changes: 29 additions & 0 deletions core/modules/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
10 changes: 10 additions & 0 deletions core/runtime/jsruntime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);

Expand Down
8 changes: 8 additions & 0 deletions core/runtime/snapshot_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion core/runtime/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 4caa187

Please sign in to comment.