Repository: denoland/deno_core
Pull request #663 (2024-03-18 02:21:56)

feat(core): addMainModuleHandler for declarative servers
Enables CloudFlare-style declarative servers:

```
core.addMainModuleHandler((module) => {
  if ("default" in module) {
    if ("fetch" in module.default) {
      serve({ handler: module.default.fetch });
    }
  }
});
```

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/denoland/deno_core/pull/663?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) Report
Attention: Patch coverage is `98.00000%` with `1 lines` in your changes are missing coverage. Please review.
> Project coverage is 80.94%. Comparing base [(`d8e1306`)](https://app.codecov.io/gh/denoland/deno_core/commit/d8e13061571e587b92487d391861faa40bd84a6f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) to head [(`bb2b537`)](https://app.codecov.io/gh/denoland/deno_core/pull/663?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).

| [Files](https://app.codecov.io/gh/denoland/deno_core/pull/663?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) | Patch % | Lines |
|---|---|---|
| [core/modules/map.rs](https://app.codecov.io/gh/denoland/deno_core/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland#diff-Y29yZS9tb2R1bGVzL21hcC5ycw==) | 96.42% | [1 Missing :warning: ](https://app.codecov.io/gh/denoland/deno_core/pull/663?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #663      +/-   ##
==========================================
+ Coverage   80.90%   80.94%   +0.03%     
==========================================
  Files          97       97              
  Lines       24275    24320      +45     
==========================================
+ Hits        19640    19685      +45     
  Misses       4635     4635              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/denoland/deno_core/pull/663?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=denoland).


Patch:
diff --git a/core/01_core.js b/core/01_core.js
index fd23a6d..895bd93 100644
--- a/core/01_core.js
+++ b/core/01_core.js
@@ -62,6 +62,7 @@
     op_resources,
     op_run_microtasks,
     op_serialize,
+    op_add_main_module_handler,
     op_set_handled_promise_rejection_handler,
     op_set_has_tick_scheduled,
     op_set_promise_hooks,
@@ -713,6 +714,7 @@
     opNames: () => op_op_names(),
     eventLoopHasMoreWork: () => op_event_loop_has_more_work(),
     byteLength: (str) => op_str_byte_length(str),
+    addMainModuleHandler: (handler) => op_add_main_module_handler(handler),
     setHandledPromiseRejectionHandler: (handler) =>
       op_set_handled_promise_rejection_handler(handler),
     setUnhandledPromiseRejectionHandler: (handler) =>
diff --git a/core/modules/map.rs b/core/modules/map.rs
index b6646d4..7ba5a06 100644
--- a/core/modules/map.rs
+++ b/core/modules/map.rs
@@ -71,6 +71,24 @@ use super::ImportMetaResolveCallback;
 struct ModEvaluate {
   module_map: Rc<ModuleMap>,
   sender: Option<oneshot::Sender<Result<(), Error>>>,
+  module: Option<v8::Global<v8::Module>>,
+  notify: Vec<v8::Global<v8::Function>>,
+}
+
+impl ModEvaluate {
+  fn notify(&mut self, scope: &mut v8::HandleScope) {
+    if !self.notify.is_empty() {
+      let module = v8::Local::new(scope, self.module.take().unwrap());
+      let ns = module.get_module_namespace();
+      let recv = v8::undefined(scope).into();
+      let args = &[ns];
+      for notify in std::mem::take(&mut self.notify).into_iter() {
+        let notify = v8::Local::new(scope, notify);
+        notify.call(scope, recv, args);
+      }
+    }
+    _ = self.sender.take().unwrap().send(Ok(()));
+  }
 }
 
 type CodeCacheReadyCallback =
@@ -225,6 +243,10 @@ impl ModuleMap {
     self.data.borrow().is_main_module(global)
   }
 
+  pub(crate) fn is_main_module_id(&self, id: ModuleId) -> bool {
+    self.data.borrow().main_module_id == Some(id)
+  }
+
   pub(crate) fn get_name_by_module(
     &self,
     global: &v8::Global<v8::Module>,
@@ -267,7 +289,6 @@ impl ModuleMap {
     self.data.borrow().is_alias(name, requested_module_type)
   }
 
-  #[cfg(test)]
   pub(crate) fn get_data(&self) -> &RefCell<ModuleMapData> {
     &self.data
   }
@@ -1012,12 +1033,25 @@ impl ModuleMap {
       let promise = v8::Local::<v8::Promise>::try_from(value)
         .expect("Expected to get promise as module evaluation result");
 
+      // If this is a main module, claim the main module notification functions
+      let (notify, module) = if self.is_main_module_id(id) {
+        let module = Some(v8::Global::new(tc_scope, module));
+        (
+          std::mem::take(&mut self.data.borrow_mut().main_module_callbacks),
+          module,
+        )
+      } else {
+        (vec![], None)
+      };
+
       // Create a ModEvaluate instance and stash it in an external
       let evaluation = v8::External::new(
         tc_scope,
         Box::into_raw(Box::new(ModEvaluate {
           module_map: self.clone(),
           sender: Some(sender),
+          notify,
+          module,
         })) as _,
       );
 
@@ -1027,13 +1061,13 @@ impl ModuleMap {
       }
 
       let on_fulfilled = Function::builder(
-        |_scope: &mut v8::HandleScope<'_>,
+        |scope: &mut v8::HandleScope<'_>,
          args: v8::FunctionCallbackArguments<'_>,
          _rv: v8::ReturnValue| {
           let mut sender = get_sender(args.data());
           sender.module_map.pending_mod_evaluation.set(false);
           sender.module_map.module_waker.wake();
-          _ = sender.sender.take().unwrap().send(Ok(()));
+          sender.notify(scope);
         },
       )
       .data(evaluation.into())
@@ -1066,7 +1100,7 @@ impl ModuleMap {
         match promise.state() {
           PromiseState::Fulfilled => {
             // Module loaded OK
-            _ = sender.sender.take().unwrap().send(Ok(()));
+            sender.notify(tc_scope);
           }
           PromiseState::Rejected => {
             // Module was rejected
diff --git a/core/modules/module_map_data.rs b/core/modules/module_map_data.rs
index 8b55d44..f0921d7 100644
--- a/core/modules/module_map_data.rs
+++ b/core/modules/module_map_data.rs
@@ -130,6 +130,7 @@ pub(crate) struct ModuleMapData {
   pub(crate) handles_inverted: HashMap<v8::Global<v8::Module>, usize>,
   /// The handles we have loaded so far, corresponding with the [`ModuleInfo`] in `info`.
   pub(crate) handles: Vec<v8::Global<v8::Module>>,
+  pub(crate) main_module_callbacks: Vec<v8::Global<v8::Function>>,
   /// The modules we have loaded so far.
   pub(crate) info: Vec<ModuleInfo>,
   /// [`ModuleName`] to [`SymbolicModule`] for modules.
@@ -152,6 +153,7 @@ pub(crate) struct ModuleMapSnapshotData {
   main_module_id: Option<i32>,
   modules: Vec<ModuleInfo>,
   module_handles: Vec<SnapshotDataId>,
+  main_module_callbacks: Vec<SnapshotDataId>,
   by_name: Vec<(FastString, RequestedModuleType, SymbolicModule)>,
 }
 
@@ -306,6 +308,11 @@ impl ModuleMapData {
       ..Default::default()
     };
 
+    ser.main_module_callbacks = self
+      .main_module_callbacks
+      .into_iter()
+      .map(|x| data_store.register(x))
+      .collect();
     ser.module_handles = self
       .handles
       .into_iter()
@@ -330,6 +337,11 @@ impl ModuleMapData {
     self.info = data.modules;
     self.handles.reserve(data.module_handles.len());
     self.handles_inverted.reserve(data.module_handles.len());
+    self.main_module_callbacks = data
+      .main_module_callbacks
+      .into_iter()
+      .map(|x| data_store.get(scope, x))
+      .collect();
 
     for module_handle in data.module_handles {
       let id = self.handles.len();
diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs
index 9c22baf..abee5cc 100644
--- a/core/ops_builtin.rs
+++ b/core/ops_builtin.rs
@@ -87,6 +87,7 @@ builtin_ops! {
   ops_builtin_types::op_is_typed_array,
   ops_builtin_types::op_is_weak_map,
   ops_builtin_types::op_is_weak_set,
+  ops_builtin_v8::op_add_main_module_handler,
   ops_builtin_v8::op_set_handled_promise_rejection_handler,
   ops_builtin_v8::op_timer_queue,
   ops_builtin_v8::op_timer_queue_system,
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 493cf19..e664923 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -23,6 +23,18 @@ use std::rc::Rc;
 use v8::ValueDeserializerHelper;
 use v8::ValueSerializerHelper;
 
+#[op2]
+pub fn op_add_main_module_handler(
+  scope: &mut v8::HandleScope,
+  #[global] f: v8::Global<v8::Function>,
+) {
+  JsRealm::module_map_from(scope)
+    .get_data()
+    .borrow_mut()
+    .main_module_callbacks
+    .push(f);
+}
+
 #[op2]
 pub fn op_set_handled_promise_rejection_handler(
   scope: &mut v8::HandleScope,


Test patch:
diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js
index 575550f..6edc904 100644
--- a/testing/checkin/runtime/__init.js
+++ b/testing/checkin/runtime/__init.js
@@ -18,6 +18,15 @@ globalThis.setInterval = timers.setInterval;
 globalThis.clearTimeout = timers.clearTimeout;
 globalThis.clearInterval = timers.clearInterval;
 globalThis.Worker = worker.Worker;
+Deno.core.addMainModuleHandler((module) => {
+  if (onMainModuleCb) onMainModuleCb(module);
+});
+let onMainModuleCb = () => {};
+Reflect.defineProperty(globalThis, "onmainmodule", {
+  set: (cb) => {
+    onMainModuleCb = cb;
+  },
+});
 Reflect.defineProperty(globalThis, "onerror", {
   set: (cb) => {
     if (cb) {
diff --git a/testing/integration/main_module_handler/main_module_handler.js b/testing/integration/main_module_handler/main_module_handler.js
new file mode 100644
index 0000000..6dab1f3
--- /dev/null
+++ b/testing/integration/main_module_handler/main_module_handler.js
@@ -0,0 +1,11 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// The handler is set up before this main module is executed
+globalThis.onmainmodule = (main) => {
+  console.log(main);
+};
+
+export const b = 2;
+
+export default {
+  a: 1,
+};
diff --git a/testing/integration/main_module_handler/main_module_handler.out b/testing/integration/main_module_handler/main_module_handler.out
new file mode 100644
index 0000000..d2c204a
--- /dev/null
+++ b/testing/integration/main_module_handler/main_module_handler.out
@@ -0,0 +1,6 @@
+{
+  "b": 2,
+  "default": {
+    "a": 1
+  }
+}
diff --git a/testing/lib.rs b/testing/lib.rs
index 5933a29..8d61095 100644
--- a/testing/lib.rs
+++ b/testing/lib.rs
@@ -58,6 +58,7 @@ integration_test!(
   error_ext_stack,
   error_with_stack,
   error_without_stack,
+  main_module_handler,
   pending_unref_op_tla,
   smoke_test,
   timer_ref,