lib_deno_resolver/
cjs.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use std::sync::Arc;
4
5use dashmap::DashMap;
6use deno_media_type::MediaType;
7use lib_node_resolver::env::NodeResolverEnv;
8use lib_node_resolver::errors::ClosestPkgJsonError;
9use lib_node_resolver::InNpmPackageChecker;
10use lib_node_resolver::NodeModuleKind;
11use lib_node_resolver::PackageJsonResolver;
12use url::Url;
13
14/// Keeps track of what module specifiers were resolved as CJS.
15///
16/// Modules that are `.js`, `.ts`, `.jsx`, and `tsx` are only known to
17/// be CJS or ESM after they're loaded based on their contents. So these
18/// files will be "maybe CJS" until they're loaded.
19#[derive(Debug)]
20pub struct CjsTracker<TEnv: NodeResolverEnv> {
21  is_cjs_resolver: IsCjsResolver<TEnv>,
22  known: DashMap<Url, NodeModuleKind>,
23}
24
25impl<TEnv: NodeResolverEnv> CjsTracker<TEnv> {
26  pub fn new(
27    in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>,
28    pkg_json_resolver: Arc<PackageJsonResolver<TEnv>>,
29    options: IsCjsResolverOptions,
30  ) -> Self {
31    Self {
32      is_cjs_resolver: IsCjsResolver::new(
33        in_npm_pkg_checker,
34        pkg_json_resolver,
35        options,
36      ),
37      known: Default::default(),
38    }
39  }
40
41  /// Checks whether the file might be treated as CJS, but it's not for sure
42  /// yet because the source hasn't been loaded to see whether it contains
43  /// imports or exports.
44  pub fn is_maybe_cjs(
45    &self,
46    specifier: &Url,
47    media_type: MediaType,
48  ) -> Result<bool, ClosestPkgJsonError> {
49    self.treat_as_cjs_with_is_script(specifier, media_type, None)
50  }
51
52  /// Gets whether the file is CJS. If true, this is for sure
53  /// cjs because `is_script` is provided.
54  ///
55  /// `is_script` should be `true` when the contents of the file at the
56  /// provided specifier are known to be a script and not an ES module.
57  pub fn is_cjs_with_known_is_script(
58    &self,
59    specifier: &Url,
60    media_type: MediaType,
61    is_script: bool,
62  ) -> Result<bool, ClosestPkgJsonError> {
63    self.treat_as_cjs_with_is_script(specifier, media_type, Some(is_script))
64  }
65
66  fn treat_as_cjs_with_is_script(
67    &self,
68    specifier: &Url,
69    media_type: MediaType,
70    is_script: Option<bool>,
71  ) -> Result<bool, ClosestPkgJsonError> {
72    let kind = match self
73      .get_known_kind_with_is_script(specifier, media_type, is_script)
74    {
75      Some(kind) => kind,
76      None => self.is_cjs_resolver.check_based_on_pkg_json(specifier)?,
77    };
78    Ok(kind == NodeModuleKind::Cjs)
79  }
80
81  /// Gets the referrer for the specified module specifier.
82  ///
83  /// Generally the referrer should already be tracked by calling
84  /// `is_cjs_with_known_is_script` before calling this method.
85  pub fn get_referrer_kind(&self, specifier: &Url) -> NodeModuleKind {
86    if specifier.scheme() != "file" {
87      return NodeModuleKind::Esm;
88    }
89    self
90      .get_known_kind(specifier, MediaType::from_specifier(specifier))
91      .unwrap_or(NodeModuleKind::Esm)
92  }
93
94  fn get_known_kind(
95    &self,
96    specifier: &Url,
97    media_type: MediaType,
98  ) -> Option<NodeModuleKind> {
99    self.get_known_kind_with_is_script(specifier, media_type, None)
100  }
101
102  fn get_known_kind_with_is_script(
103    &self,
104    specifier: &Url,
105    media_type: MediaType,
106    is_script: Option<bool>,
107  ) -> Option<NodeModuleKind> {
108    self.is_cjs_resolver.get_known_kind_with_is_script(
109      specifier,
110      media_type,
111      is_script,
112      &self.known,
113    )
114  }
115}
116
117#[derive(Debug)]
118pub struct IsCjsResolverOptions {
119  pub detect_cjs: bool,
120  pub is_node_main: bool,
121}
122
123/// Resolves whether a module is CJS or ESM.
124#[derive(Debug)]
125pub struct IsCjsResolver<TEnv: NodeResolverEnv> {
126  in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>,
127  pkg_json_resolver: Arc<PackageJsonResolver<TEnv>>,
128  options: IsCjsResolverOptions,
129}
130
131impl<TEnv: NodeResolverEnv> IsCjsResolver<TEnv> {
132  pub fn new(
133    in_npm_pkg_checker: Arc<dyn InNpmPackageChecker>,
134    pkg_json_resolver: Arc<PackageJsonResolver<TEnv>>,
135    options: IsCjsResolverOptions,
136  ) -> Self {
137    Self {
138      in_npm_pkg_checker,
139      pkg_json_resolver,
140      options,
141    }
142  }
143
144  /// Gets the referrer kind for a script in the LSP.
145  pub fn get_lsp_referrer_kind(
146    &self,
147    specifier: &Url,
148    is_script: Option<bool>,
149  ) -> NodeModuleKind {
150    if specifier.scheme() != "file" {
151      return NodeModuleKind::Esm;
152    }
153    match MediaType::from_specifier(specifier) {
154      MediaType::Mts | MediaType::Mjs | MediaType::Dmts => NodeModuleKind::Esm,
155      MediaType::Cjs | MediaType::Cts | MediaType::Dcts => NodeModuleKind::Cjs,
156      MediaType::Dts => {
157        // dts files are always determined based on the package.json because
158        // they contain imports/exports even when considered CJS
159        self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm)
160      }
161      MediaType::Wasm |
162      MediaType::Json => NodeModuleKind::Esm,
163      MediaType::JavaScript
164      | MediaType::Jsx
165      | MediaType::TypeScript
166      | MediaType::Tsx
167      // treat these as unknown
168      | MediaType::Css
169      | MediaType::SourceMap
170      | MediaType::Unknown => {
171        match is_script {
172          Some(true) => self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm),
173          Some(false) | None => NodeModuleKind::Esm,
174        }
175      }
176    }
177  }
178
179  fn get_known_kind_with_is_script(
180    &self,
181    specifier: &Url,
182    media_type: MediaType,
183    is_script: Option<bool>,
184    known_cache: &DashMap<Url, NodeModuleKind>,
185  ) -> Option<NodeModuleKind> {
186    if specifier.scheme() != "file" {
187      return Some(NodeModuleKind::Esm);
188    }
189
190    match media_type {
191      MediaType::Mts | MediaType::Mjs | MediaType::Dmts => Some(NodeModuleKind::Esm),
192      MediaType::Cjs | MediaType::Cts | MediaType::Dcts => Some(NodeModuleKind::Cjs),
193      MediaType::Dts => {
194        // dts files are always determined based on the package.json because
195        // they contain imports/exports even when considered CJS
196        if let Some(value) = known_cache.get(specifier).map(|v| *v) {
197          Some(value)
198        } else {
199          let value = self.check_based_on_pkg_json(specifier).ok();
200          if let Some(value) = value {
201            known_cache.insert(specifier.clone(), value);
202          }
203          Some(value.unwrap_or(NodeModuleKind::Esm))
204        }
205      }
206      MediaType::Wasm |
207      MediaType::Json => Some(NodeModuleKind::Esm),
208      MediaType::JavaScript
209      | MediaType::Jsx
210      | MediaType::TypeScript
211      | MediaType::Tsx
212      // treat these as unknown
213      | MediaType::Css
214      | MediaType::SourceMap
215      | MediaType::Unknown => {
216        if let Some(value) = known_cache.get(specifier).map(|v| *v) {
217          if value == NodeModuleKind::Cjs && is_script == Some(false) {
218            // we now know this is actually esm
219            known_cache.insert(specifier.clone(), NodeModuleKind::Esm);
220            Some(NodeModuleKind::Esm)
221          } else {
222            Some(value)
223          }
224        } else if is_script == Some(false) {
225          // we know this is esm
226            known_cache.insert(specifier.clone(), NodeModuleKind::Esm);
227          Some(NodeModuleKind::Esm)
228        } else {
229          None
230        }
231      }
232    }
233  }
234
235  fn check_based_on_pkg_json(
236    &self,
237    specifier: &Url,
238  ) -> Result<NodeModuleKind, ClosestPkgJsonError> {
239    if self.in_npm_pkg_checker.in_npm_package(specifier) {
240      if let Some(pkg_json) =
241        self.pkg_json_resolver.get_closest_package_json(specifier)?
242      {
243        let is_file_location_cjs = pkg_json.typ != "module";
244        Ok(if is_file_location_cjs {
245          NodeModuleKind::Cjs
246        } else {
247          NodeModuleKind::Esm
248        })
249      } else {
250        Ok(NodeModuleKind::Cjs)
251      }
252    } else if self.options.detect_cjs || self.options.is_node_main {
253      if let Some(pkg_json) =
254        self.pkg_json_resolver.get_closest_package_json(specifier)?
255      {
256        let is_cjs_type = pkg_json.typ == "commonjs"
257          || self.options.is_node_main && pkg_json.typ == "none";
258        Ok(if is_cjs_type {
259          NodeModuleKind::Cjs
260        } else {
261          NodeModuleKind::Esm
262        })
263      } else if self.options.is_node_main {
264        Ok(NodeModuleKind::Cjs)
265      } else {
266        Ok(NodeModuleKind::Esm)
267      }
268    } else {
269      Ok(NodeModuleKind::Esm)
270    }
271  }
272}