rw_deno_core/modules/
mod.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2use crate::error::exception_to_err_result;
3use crate::error::AnyError;
4use crate::fast_string::FastString;
5use crate::module_specifier::ModuleSpecifier;
6use crate::FastStaticString;
7use anyhow::bail;
8use anyhow::Context;
9use anyhow::Error;
10use serde::Deserialize;
11use serde::Serialize;
12use std::borrow::Cow;
13use std::collections::HashMap;
14use std::future::Future;
15use std::sync::Arc;
16use url::Url;
17
18mod loaders;
19mod map;
20mod module_map_data;
21mod recursive_load;
22
23#[cfg(all(test, not(miri)))]
24mod tests;
25
26pub(crate) use loaders::ExtModuleLoader;
27pub use loaders::ExtModuleLoaderCb;
28pub use loaders::FsModuleLoader;
29pub(crate) use loaders::LazyEsmModuleLoader;
30pub use loaders::ModuleLoadResponse;
31pub use loaders::ModuleLoader;
32pub use loaders::NoopModuleLoader;
33pub use loaders::StaticModuleLoader;
34pub(crate) use map::synthetic_module_evaluation_steps;
35pub(crate) use map::ModuleMap;
36pub(crate) use module_map_data::ModuleMapSnapshotData;
37
38pub type ModuleId = usize;
39pub(crate) type ModuleLoadId = i32;
40
41/// The actual source code returned from the loader. Most embedders should
42/// try to return bytes and let deno_core interpret if the module should be
43/// converted to a string or not.
44#[derive(Debug)]
45pub enum ModuleSourceCode {
46  String(ModuleCodeString),
47  Bytes(ModuleCodeBytes),
48}
49
50pub type ModuleCodeString = FastString;
51pub type ModuleName = FastString;
52
53/// Converts various string-like things into `ModuleName`.
54pub trait IntoModuleName {
55  fn into_module_name(self) -> ModuleName;
56}
57
58impl IntoModuleName for ModuleName {
59  fn into_module_name(self) -> ModuleName {
60    self
61  }
62}
63
64impl IntoModuleName for &'static str {
65  fn into_module_name(self) -> ModuleName {
66    ModuleName::from_static(self)
67  }
68}
69
70impl IntoModuleName for String {
71  fn into_module_name(self) -> ModuleName {
72    ModuleName::from(self)
73  }
74}
75
76impl IntoModuleName for Url {
77  fn into_module_name(self) -> ModuleName {
78    ModuleName::from(self)
79  }
80}
81
82impl IntoModuleName for FastStaticString {
83  fn into_module_name(self) -> ModuleName {
84    ModuleName::from(self)
85  }
86}
87
88/// Converts various string-like things into `ModuleCodeString`.
89pub trait IntoModuleCodeString {
90  fn into_module_code(self) -> ModuleCodeString;
91}
92
93impl IntoModuleCodeString for ModuleCodeString {
94  fn into_module_code(self) -> ModuleCodeString {
95    self
96  }
97}
98
99impl IntoModuleCodeString for &'static str {
100  fn into_module_code(self) -> ModuleCodeString {
101    ModuleCodeString::from_static(self)
102  }
103}
104
105impl IntoModuleCodeString for String {
106  fn into_module_code(self) -> ModuleCodeString {
107    ModuleCodeString::from(self)
108  }
109}
110
111impl IntoModuleCodeString for FastStaticString {
112  fn into_module_code(self) -> ModuleCodeString {
113    ModuleCodeString::from(self)
114  }
115}
116
117impl IntoModuleCodeString for Arc<str> {
118  fn into_module_code(self) -> ModuleCodeString {
119    ModuleCodeString::from(self)
120  }
121}
122
123#[derive(Debug)]
124pub enum ModuleCodeBytes {
125  /// Created from static data.
126  Static(&'static [u8]),
127
128  /// An owned chunk of data. Note that we use `Box` rather than `Vec` to avoid
129  /// the storage overhead.
130  Boxed(Box<[u8]>),
131
132  /// Code loaded from the `deno_graph` infrastructure.
133  Arc(Arc<[u8]>),
134}
135
136impl ModuleCodeBytes {
137  pub fn as_bytes(&self) -> &[u8] {
138    match self {
139      ModuleCodeBytes::Static(s) => s,
140      ModuleCodeBytes::Boxed(s) => s,
141      ModuleCodeBytes::Arc(s) => s,
142    }
143  }
144
145  pub fn to_vec(&self) -> Vec<u8> {
146    match self {
147      ModuleCodeBytes::Static(s) => s.to_vec(),
148      ModuleCodeBytes::Boxed(s) => s.to_vec(),
149      ModuleCodeBytes::Arc(s) => s.to_vec(),
150    }
151  }
152}
153
154impl From<Arc<[u8]>> for ModuleCodeBytes {
155  fn from(value: Arc<[u8]>) -> Self {
156    Self::Arc(value)
157  }
158}
159
160impl From<Box<[u8]>> for ModuleCodeBytes {
161  fn from(value: Box<[u8]>) -> Self {
162    Self::Boxed(value)
163  }
164}
165
166impl From<&'static [u8]> for ModuleCodeBytes {
167  fn from(value: &'static [u8]) -> Self {
168    Self::Static(value)
169  }
170}
171
172/// Callback to customize value of `import.meta.resolve("./foo.ts")`.
173pub type ImportMetaResolveCallback = Box<
174  dyn Fn(&dyn ModuleLoader, String, String) -> Result<ModuleSpecifier, Error>,
175>;
176
177pub(crate) fn default_import_meta_resolve_cb(
178  loader: &dyn ModuleLoader,
179  specifier: String,
180  referrer: String,
181) -> Result<ModuleSpecifier, Error> {
182  if specifier.starts_with("npm:") {
183    bail!("\"npm:\" specifiers are currently not supported in import.meta.resolve()");
184  }
185
186  loader.resolve(&specifier, &referrer, ResolutionKind::DynamicImport)
187}
188
189/// Callback to validate import attributes. If the validation fails and exception
190/// should be thrown using `scope.throw_exception()`.
191pub type ValidateImportAttributesCb =
192  Box<dyn Fn(&mut v8::HandleScope, &HashMap<String, String>)>;
193
194/// Callback to validate import attributes. If the validation fails and exception
195/// should be thrown using `scope.throw_exception()`.
196pub type CustomModuleEvaluationCb = Box<
197  dyn Fn(
198    &mut v8::HandleScope,
199    Cow<'_, str>,
200    &FastString,
201    ModuleSourceCode,
202  ) -> Result<CustomModuleEvaluationKind, AnyError>,
203>;
204
205pub type EvalContextGetCodeCacheCb =
206  Box<dyn Fn(&str) -> Result<Option<Cow<'static, [u8]>>, AnyError>>;
207
208pub type EvalContextCodeCacheReadyCb = Box<dyn Fn(&str, &[u8])>;
209
210pub enum CustomModuleEvaluationKind {
211  /// This evaluation results in a single, "synthetic" module.
212  Synthetic(v8::Global<v8::Value>),
213
214  /// This evaluation results in creation of two modules:
215  ///  - a "computed" module - some JavaScript that most likely is rendered and
216  ///    uses the "synthetic" module - this module's ID is returned from
217  ///    [`new_module`] call.
218  ///  - a "synthetic" module - a kind of a helper module that abstracts
219  ///    the source of JS objects - this module is set up first.
220  ComputedAndSynthetic(
221    // Source code of computed module,
222    FastString,
223    // Synthetic module value
224    v8::Global<v8::Value>,
225    // Synthetic module type
226    ModuleType,
227  ),
228}
229
230#[derive(Debug)]
231pub(crate) enum ImportAttributesKind {
232  StaticImport,
233  DynamicImport,
234}
235
236pub(crate) fn parse_import_attributes(
237  scope: &mut v8::HandleScope,
238  attributes: v8::Local<v8::FixedArray>,
239  kind: ImportAttributesKind,
240) -> HashMap<String, String> {
241  let mut assertions: HashMap<String, String> = HashMap::default();
242
243  let assertions_per_line = match kind {
244    // For static imports, assertions are triples of (keyword, value and source offset)
245    // Also used in `module_resolve_callback`.
246    ImportAttributesKind::StaticImport => 3,
247    // For dynamic imports, assertions are tuples of (keyword, value)
248    ImportAttributesKind::DynamicImport => 2,
249  };
250  assert_eq!(attributes.length() % assertions_per_line, 0);
251  let no_of_assertions = attributes.length() / assertions_per_line;
252
253  for i in 0..no_of_assertions {
254    let assert_key = attributes.get(scope, assertions_per_line * i).unwrap();
255    let assert_key_val = v8::Local::<v8::Value>::try_from(assert_key).unwrap();
256    let assert_value = attributes
257      .get(scope, (assertions_per_line * i) + 1)
258      .unwrap();
259    let assert_value_val =
260      v8::Local::<v8::Value>::try_from(assert_value).unwrap();
261    assertions.insert(
262      assert_key_val.to_rust_string_lossy(scope),
263      assert_value_val.to_rust_string_lossy(scope),
264    );
265  }
266
267  assertions
268}
269
270pub(crate) fn get_requested_module_type_from_attributes(
271  attributes: &HashMap<String, String>,
272) -> RequestedModuleType {
273  let Some(ty) = attributes.get("type") else {
274    return RequestedModuleType::None;
275  };
276
277  if ty == "json" {
278    RequestedModuleType::Json
279  } else {
280    RequestedModuleType::Other(Cow::Owned(ty.to_string()))
281  }
282}
283
284/// A type of module to be executed.
285///
286/// `deno_core` supports loading and executing JavaScript, Wasm and JSON modules,
287/// by default, but embedders can customize it further by providing
288/// [`CustomModuleEvaluationCb`].
289#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
290#[repr(u32)]
291pub enum ModuleType {
292  JavaScript,
293  Wasm,
294  Json,
295  Other(Cow<'static, str>),
296}
297
298impl std::fmt::Display for ModuleType {
299  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
300    match self {
301      Self::JavaScript => write!(f, "JavaScript"),
302      Self::Wasm => write!(f, "Wasm"),
303      Self::Json => write!(f, "JSON"),
304      Self::Other(ty) => write!(f, "{}", ty),
305    }
306  }
307}
308
309impl ModuleType {
310  pub fn to_v8<'s>(
311    &self,
312    scope: &mut v8::HandleScope<'s>,
313  ) -> v8::Local<'s, v8::Value> {
314    match self {
315      ModuleType::JavaScript => v8::Integer::new(scope, 0).into(),
316      ModuleType::Wasm => v8::Integer::new(scope, 1).into(),
317      ModuleType::Json => v8::Integer::new(scope, 2).into(),
318      ModuleType::Other(ty) => v8::String::new(scope, ty).unwrap().into(),
319    }
320  }
321
322  pub fn try_from_v8(
323    scope: &mut v8::HandleScope,
324    value: v8::Local<v8::Value>,
325  ) -> Option<Self> {
326    Some(if let Some(int) = value.to_integer(scope) {
327      match int.int32_value(scope).unwrap_or_default() {
328        0 => ModuleType::JavaScript,
329        1 => ModuleType::Wasm,
330        2 => ModuleType::Json,
331        _ => return None,
332      }
333    } else if let Ok(str) = v8::Local::<v8::String>::try_from(value) {
334      ModuleType::Other(Cow::Owned(str.to_rust_string_lossy(scope)))
335    } else {
336      return None;
337    })
338  }
339}
340
341/// EsModule source code that will be loaded into V8.
342///
343/// Users can implement `Into<ModuleInfo>` for different file types that
344/// can be transpiled to valid EsModule.
345///
346/// Found module URL might be different from specified URL
347/// used for loading due to redirections (like HTTP 303).
348/// Eg. Both "`https://example.com/a.ts`" and
349/// "`https://example.com/b.ts`" may point to "`https://example.com/c.ts`"
350/// By keeping track of specified and found URL we can alias modules and avoid
351/// recompiling the same code 3 times.
352// TODO(bartlomieju): I have a strong opinion we should store all redirects
353// that happened; not only first and final target. It would simplify a lot
354// of things throughout the codebase otherwise we may end up requesting
355// intermediate redirects from file loader.
356// NOTE: This should _not_ be made #[derive(Clone)] unless we take some precautions to avoid excessive string copying.
357#[derive(Debug)]
358pub struct ModuleSource {
359  pub code: ModuleSourceCode,
360  pub module_type: ModuleType,
361  pub code_cache: Option<Cow<'static, [u8]>>,
362  module_url_specified: ModuleName,
363  /// If the module was found somewhere other than the specified address, this will be [`Some`].
364  module_url_found: Option<ModuleName>,
365}
366
367impl ModuleSource {
368  /// Create a [`ModuleSource`] without a redirect.
369  pub fn new(
370    module_type: impl Into<ModuleType>,
371    code: ModuleSourceCode,
372    specifier: &ModuleSpecifier,
373    code_cache: Option<Cow<'static, [u8]>>,
374  ) -> Self {
375    let module_url_specified = specifier.as_ref().to_owned().into();
376    Self {
377      code,
378      module_type: module_type.into(),
379      code_cache,
380      module_url_specified,
381      module_url_found: None,
382    }
383  }
384
385  /// Create a [`ModuleSource`] with a potential redirect. If the `specifier_found` parameter is the same as the
386  /// specifier, the code behaves the same was as `ModuleSource::new`.
387  pub fn new_with_redirect(
388    module_type: impl Into<ModuleType>,
389    code: ModuleSourceCode,
390    specifier: &ModuleSpecifier,
391    specifier_found: &ModuleSpecifier,
392    code_cache: Option<Cow<'static, [u8]>>,
393  ) -> Self {
394    let module_url_found = if specifier == specifier_found {
395      None
396    } else {
397      Some(specifier_found.as_ref().to_owned().into())
398    };
399    let module_url_specified = specifier.as_ref().to_owned().into();
400    Self {
401      code,
402      module_type: module_type.into(),
403      code_cache,
404      module_url_specified,
405      module_url_found,
406    }
407  }
408
409  #[cfg(test)]
410  pub fn for_test(code: &'static str, file: impl AsRef<str>) -> Self {
411    Self {
412      code: ModuleSourceCode::String(code.into_module_code()),
413      module_type: ModuleType::JavaScript,
414      code_cache: None,
415      module_url_specified: file.as_ref().to_owned().into(),
416      module_url_found: None,
417    }
418  }
419
420  /// If the `found` parameter is the same as the `specified` parameter, the code behaves the same was as `ModuleSource::for_test`.
421  #[cfg(test)]
422  pub fn for_test_with_redirect(
423    code: &'static str,
424    specified: impl AsRef<str>,
425    found: impl AsRef<str>,
426    code_cache: Option<Cow<'static, [u8]>>,
427  ) -> Self {
428    let specified = specified.as_ref().to_string();
429    let found = found.as_ref().to_string();
430    let found = if found == specified {
431      None
432    } else {
433      Some(found.into())
434    };
435    Self {
436      code: ModuleSourceCode::String(code.into_module_code()),
437      module_type: ModuleType::JavaScript,
438      code_cache,
439      module_url_specified: specified.into(),
440      module_url_found: found,
441    }
442  }
443
444  pub fn get_string_source(
445    specifier: &str,
446    code: ModuleSourceCode,
447  ) -> Result<ModuleCodeString, AnyError> {
448    match code {
449      ModuleSourceCode::String(code) => Ok(code),
450      ModuleSourceCode::Bytes(bytes) => {
451        let str_ = String::from_utf8(bytes.to_vec()).with_context(|| {
452          format!("Can't convert source code to string for {}", specifier)
453        })?;
454        Ok(ModuleCodeString::from(str_))
455      }
456    }
457  }
458}
459
460pub type ModuleSourceFuture = dyn Future<Output = Result<ModuleSource, Error>>;
461
462#[derive(Debug, PartialEq, Eq)]
463pub enum ResolutionKind {
464  /// This kind is used in only one situation: when a module is loaded via
465  /// `JsRuntime::load_main_module` and is the top-level module, ie. the one
466  /// passed as an argument to `JsRuntime::load_main_module`.
467  MainModule,
468  /// This kind is returned for all other modules during module load, that are
469  /// static imports.
470  Import,
471  /// This kind is returned for all modules that are loaded as a result of a
472  /// call to `import()` API (ie. top-level module as well as all its
473  /// dependencies, and any other `import()` calls from that load).
474  DynamicImport,
475}
476
477#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
478#[repr(u8)]
479pub enum RequestedModuleType {
480  /// There was no attribute specified in the import statement.
481  ///
482  /// Example:
483  /// ```ignore
484  /// import foo from "./foo.js";
485  ///
486  /// const bar = await import("bar");
487  /// ```
488  None,
489
490  /// The `type` attribute had value `json`. This is the only known module type
491  /// in `deno_core`.
492  ///
493  /// Embedders should use `Other` variant for custom module
494  /// types like `wasm`, `bytes` or `text`.
495  ///
496  /// Example:
497  /// ```ignore
498  /// import jsonData from "./data.json" with { type: "json" };
499  ///
500  /// const jsonData2 = await import"./data2.json", { with { type: "json" } });
501  /// ```
502  Json,
503
504  /// An arbitrary module type. It is up to the embedder to handle (or deny) it.
505  /// If [`CustomModuleEvaluationCb`] was not passed when creating a runtime,
506  /// then all "other" module types cause an error to be returned.
507  ///
508  /// Example:
509  /// ```ignore
510  /// import text from "./log.txt" with { type: "text" };
511  ///
512  /// const imgData = await import(`./images/${name}.png`, { with: { type: "bytes" }});
513  /// ```
514  Other(Cow<'static, str>),
515}
516
517impl RequestedModuleType {
518  pub fn to_v8<'s>(
519    &self,
520    scope: &mut v8::HandleScope<'s>,
521  ) -> v8::Local<'s, v8::Value> {
522    match self {
523      RequestedModuleType::None => v8::Integer::new(scope, 0).into(),
524      RequestedModuleType::Json => v8::Integer::new(scope, 1).into(),
525      RequestedModuleType::Other(ty) => {
526        v8::String::new(scope, ty).unwrap().into()
527      }
528    }
529  }
530
531  pub fn try_from_v8(
532    scope: &mut v8::HandleScope,
533    value: v8::Local<v8::Value>,
534  ) -> Option<Self> {
535    Some(if let Some(int) = value.to_integer(scope) {
536      match int.int32_value(scope).unwrap_or_default() {
537        0 => RequestedModuleType::None,
538        1 => RequestedModuleType::Json,
539        _ => return None,
540      }
541    } else if let Ok(str) = v8::Local::<v8::String>::try_from(value) {
542      RequestedModuleType::Other(Cow::Owned(str.to_rust_string_lossy(scope)))
543    } else {
544      return None;
545    })
546  }
547}
548
549impl AsRef<RequestedModuleType> for RequestedModuleType {
550  fn as_ref(&self) -> &RequestedModuleType {
551    self
552  }
553}
554
555// TODO(bartlomieju): this is questionable. I think we should remove it.
556impl PartialEq<ModuleType> for RequestedModuleType {
557  fn eq(&self, other: &ModuleType) -> bool {
558    match other {
559      ModuleType::JavaScript => self == &RequestedModuleType::None,
560      ModuleType::Wasm => self == &RequestedModuleType::None,
561      ModuleType::Json => self == &RequestedModuleType::Json,
562      ModuleType::Other(ty) => self == &RequestedModuleType::Other(ty.clone()),
563    }
564  }
565}
566
567impl From<ModuleType> for RequestedModuleType {
568  fn from(module_type: ModuleType) -> RequestedModuleType {
569    match module_type {
570      ModuleType::JavaScript => RequestedModuleType::None,
571      ModuleType::Wasm => RequestedModuleType::None,
572      ModuleType::Json => RequestedModuleType::Json,
573      ModuleType::Other(ty) => RequestedModuleType::Other(ty.clone()),
574    }
575  }
576}
577
578impl std::fmt::Display for RequestedModuleType {
579  fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
580    match self {
581      Self::None => write!(f, "None"),
582      Self::Json => write!(f, "JSON"),
583      Self::Other(ty) => write!(f, "Other({ty})"),
584    }
585  }
586}
587
588/// Describes a request for a module as parsed from the source code.
589/// Usually executable (`JavaScriptOrWasm`) is used, except when an
590/// import assertions explicitly constrains an import to JSON, in
591/// which case this will have a `RequestedModuleType::Json`.
592#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
593pub(crate) struct ModuleRequest {
594  pub specifier: String,
595  pub requested_module_type: RequestedModuleType,
596}
597
598#[derive(Debug, PartialEq, Serialize, Deserialize)]
599pub(crate) struct ModuleInfo {
600  #[allow(unused)]
601  pub id: ModuleId,
602  pub main: bool,
603  pub name: ModuleName,
604  pub requests: Vec<ModuleRequest>,
605  pub module_type: ModuleType,
606}
607
608#[derive(Debug)]
609pub(crate) enum ModuleError {
610  Exception(v8::Global<v8::Value>),
611  Other(Error),
612}
613
614impl ModuleError {
615  pub fn into_any_error(
616    self,
617    scope: &mut v8::HandleScope,
618    in_promise: bool,
619    clear_error: bool,
620  ) -> AnyError {
621    match self {
622      ModuleError::Exception(exception) => {
623        let exception = v8::Local::new(scope, exception);
624        exception_to_err_result::<()>(scope, exception, in_promise, clear_error)
625          .unwrap_err()
626      }
627      ModuleError::Other(error) => error,
628    }
629  }
630}