|
4 | 4 |
|
5 | 5 | use crate::wasi_job_executor::WasiJobExecutor; |
6 | 6 | use boa_engine::{ |
7 | | - Context, JsError, Source, |
| 7 | + Context, JsError, JsResult, JsValue, Source, |
8 | 8 | builtins::promise::PromiseState, |
9 | | - module::{IdleModuleLoader, Module}, |
10 | | - object::builtins::JsFunction, |
| 9 | + module::Module, |
| 10 | + object::builtins::{JsFunction, JsPromise}, |
11 | 11 | }; |
12 | 12 | use std::cell::RefCell; |
13 | 13 | use std::rc::Rc; |
@@ -39,102 +39,21 @@ impl EsmError { |
39 | 39 | /// Parse an ES module and extract its default export as a callable function. |
40 | 40 | /// |
41 | 41 | /// This function: |
42 | | -/// 1. Uses `IdleModuleLoader` (rejects all imports - fail fast) |
43 | | -/// 2. Parses the JS code as an ES Module |
44 | | -/// 3. Loads module dependencies (should resolve immediately with no imports) |
45 | | -/// 4. Links the module |
46 | | -/// 5. Evaluates the module |
47 | | -/// 6. Extracts the `default` export from the module namespace |
48 | | -/// 7. Verifies it's a callable function |
| 42 | +/// 1. Parses the JS code as an ES Module |
| 43 | +/// 2. Loads module dependencies (should resolve immediately with no imports) |
| 44 | +/// 3. Links the module |
| 45 | +/// 4. Evaluates the module |
| 46 | +/// 5. Extracts the `default` export from the module namespace |
| 47 | +/// 6. Verifies it's a callable function |
49 | 48 | /// |
50 | 49 | /// # Arguments |
51 | 50 | /// * `js_code` - JavaScript source code with `export default function(...) { ... }` |
52 | | -/// * `context` - Boa JS context (must be configured before calling) |
| 51 | +/// * `context` - Boa JS context wrapped in RefCell |
| 52 | +/// * `executor` - The WasiJobExecutor for driving async jobs |
53 | 53 | /// |
54 | 54 | /// # Returns |
55 | 55 | /// The default export as a `JsFunction`, or an `EsmError` if any step fails. |
56 | | -pub fn get_default_export(js_code: &str, context: &mut Context) -> Result<JsFunction, EsmError> { |
57 | | - // Ensure we use IdleModuleLoader which rejects any imports |
58 | | - // Note: The context should already be configured with IdleModuleLoader by default, |
59 | | - // but we explicitly set it here to be safe. |
60 | | - let _loader = Rc::new(IdleModuleLoader); |
61 | | - // Context's module_loader is already set in the builder, we rely on that. |
62 | | - |
63 | | - // 1. Parse the JS code as an ES Module |
64 | | - let module = Module::parse(Source::from_bytes(js_code), None, context) |
65 | | - .map_err(|err| EsmError::from_js_error(err, EsmError::ParseError))?; |
66 | | - |
67 | | - // 2. Load module dependencies |
68 | | - let load_promise = module.load(context); |
69 | | - |
70 | | - // Drive the load promise to completion (should resolve immediately with no imports) |
71 | | - context |
72 | | - .run_jobs() |
73 | | - .map_err(|err| EsmError::from_js_error(err, EsmError::LoadError))?; |
74 | | - |
75 | | - match load_promise.state() { |
76 | | - PromiseState::Fulfilled(_) => {} |
77 | | - PromiseState::Rejected(err) => { |
78 | | - return Err(EsmError::LoadError(JsError::from_opaque(err).to_string())); |
79 | | - } |
80 | | - PromiseState::Pending => { |
81 | | - return Err(EsmError::LoadError( |
82 | | - "module load promise is still pending".to_string(), |
83 | | - )); |
84 | | - } |
85 | | - } |
86 | | - |
87 | | - // 3. Link the module |
88 | | - module |
89 | | - .link(context) |
90 | | - .map_err(|err| EsmError::from_js_error(err, EsmError::LinkError))?; |
91 | | - |
92 | | - // 4. Evaluate the module |
93 | | - let eval_promise = module.evaluate(context); |
94 | | - |
95 | | - // Drive the evaluate promise to completion |
96 | | - context |
97 | | - .run_jobs() |
98 | | - .map_err(|err| EsmError::from_js_error(err, EsmError::EvalError))?; |
99 | | - |
100 | | - match eval_promise.state() { |
101 | | - PromiseState::Fulfilled(_) => {} |
102 | | - PromiseState::Rejected(err) => { |
103 | | - return Err(EsmError::EvalError(JsError::from_opaque(err).to_string())); |
104 | | - } |
105 | | - PromiseState::Pending => { |
106 | | - return Err(EsmError::EvalError( |
107 | | - "module evaluate promise is still pending".to_string(), |
108 | | - )); |
109 | | - } |
110 | | - } |
111 | | - |
112 | | - // 5. Get the module namespace and extract the default export |
113 | | - let namespace = module.namespace(context); |
114 | | - let default_export = namespace |
115 | | - .get(boa_engine::js_string!("default"), context) |
116 | | - .map_err(|err| EsmError::from_js_error(err, EsmError::EvalError))?; |
117 | | - |
118 | | - // 6. Check if default export exists and is undefined |
119 | | - if default_export.is_undefined() { |
120 | | - return Err(EsmError::NoDefaultExport); |
121 | | - } |
122 | | - |
123 | | - // 7. Verify it's a callable function |
124 | | - let Some(func) = default_export.as_callable() else { |
125 | | - return Err(EsmError::DefaultNotCallable); |
126 | | - }; |
127 | | - |
128 | | - // Convert JsObject to JsFunction |
129 | | - JsFunction::from_object(func.clone()).ok_or(EsmError::DefaultNotCallable) |
130 | | -} |
131 | | - |
132 | | -/// Async version of [`get_default_export`] for use in async contexts. |
133 | | -/// |
134 | | -/// This version uses `WasiJobExecutor::drive_jobs` instead of `context.run_jobs()`, |
135 | | -/// which avoids nesting `block_on` calls when already inside wstd's async runtime |
136 | | -/// (e.g., in webhook handlers). |
137 | | -pub async fn get_default_export_async( |
| 56 | +pub async fn get_default_export( |
138 | 57 | js_code: &str, |
139 | 58 | context: &RefCell<&mut Context>, |
140 | 59 | executor: &Rc<WasiJobExecutor>, |
@@ -210,3 +129,36 @@ pub async fn get_default_export_async( |
210 | 129 |
|
211 | 130 | JsFunction::from_object(func.clone()).ok_or(EsmError::DefaultNotCallable) |
212 | 131 | } |
| 132 | + |
| 133 | +/// If `value` is a Promise, drive it to completion and return the resolved value. |
| 134 | +/// |
| 135 | +/// This function drives the executor until the specific promise resolves, |
| 136 | +/// then returns immediately (abandoning any orphaned jobs like unwaited timers). |
| 137 | +pub async fn resolve_promise( |
| 138 | + value: &JsValue, |
| 139 | + context: &RefCell<&mut Context>, |
| 140 | + executor: &Rc<WasiJobExecutor>, |
| 141 | +) -> JsResult<JsValue> { |
| 142 | + let Some(object) = value.as_object() else { |
| 143 | + return Ok(value.clone()); |
| 144 | + }; |
| 145 | + let Ok(promise) = JsPromise::from_object(object) else { |
| 146 | + return Ok(value.clone()); |
| 147 | + }; |
| 148 | + |
| 149 | + // Drive jobs until this specific promise resolves, then stop immediately. |
| 150 | + // This abandons orphaned jobs (like unwaited setTimeout callbacks). |
| 151 | + executor |
| 152 | + .clone() |
| 153 | + .drive_jobs_until(context, || { |
| 154 | + !matches!(promise.state(), PromiseState::Pending) |
| 155 | + }) |
| 156 | + .await?; |
| 157 | + |
| 158 | + // Return the resolved value |
| 159 | + match promise.state() { |
| 160 | + PromiseState::Fulfilled(v) => Ok(v), |
| 161 | + PromiseState::Rejected(e) => Err(JsError::from_opaque(e)), |
| 162 | + PromiseState::Pending => unreachable!("promise should be resolved after drive_jobs_until"), |
| 163 | + } |
| 164 | +} |
0 commit comments