// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::sync::Arc; use dashmap::DashMap; use deno_media_type::MediaType; use node_resolver::env::NodeResolverEnv; use node_resolver::errors::ClosestPkgJsonError; use node_resolver::InNpmPackageChecker; use node_resolver::NodeModuleKind; use node_resolver::PackageJsonResolver; use url::Url; /// Keeps track of what module specifiers were resolved as CJS. /// /// Modules that are `.js`, `.ts`, `.jsx`, and `tsx` are only known to /// be CJS or ESM after they're loaded based on their contents. So these /// files will be "maybe CJS" until they're loaded. #[derive(Debug)] pub struct CjsTracker { is_cjs_resolver: IsCjsResolver, known: DashMap, } impl CjsTracker { pub fn new( in_npm_pkg_checker: Arc, pkg_json_resolver: Arc>, options: IsCjsResolverOptions, ) -> Self { Self { is_cjs_resolver: IsCjsResolver::new( in_npm_pkg_checker, pkg_json_resolver, options, ), known: Default::default(), } } /// Checks whether the file might be treated as CJS, but it's not for sure /// yet because the source hasn't been loaded to see whether it contains /// imports or exports. pub fn is_maybe_cjs( &self, specifier: &Url, media_type: MediaType, ) -> Result { self.treat_as_cjs_with_is_script(specifier, media_type, None) } /// Gets whether the file is CJS. If true, this is for sure /// cjs because `is_script` is provided. /// /// `is_script` should be `true` when the contents of the file at the /// provided specifier are known to be a script and not an ES module. pub fn is_cjs_with_known_is_script( &self, specifier: &Url, media_type: MediaType, is_script: bool, ) -> Result { self.treat_as_cjs_with_is_script(specifier, media_type, Some(is_script)) } fn treat_as_cjs_with_is_script( &self, specifier: &Url, media_type: MediaType, is_script: Option, ) -> Result { let kind = match self .get_known_kind_with_is_script(specifier, media_type, is_script) { Some(kind) => kind, None => self.is_cjs_resolver.check_based_on_pkg_json(specifier)?, }; Ok(kind == NodeModuleKind::Cjs) } /// Gets the referrer for the specified module specifier. /// /// Generally the referrer should already be tracked by calling /// `is_cjs_with_known_is_script` before calling this method. pub fn get_referrer_kind(&self, specifier: &Url) -> NodeModuleKind { if specifier.scheme() != "file" { return NodeModuleKind::Esm; } self .get_known_kind(specifier, MediaType::from_specifier(specifier)) .unwrap_or(NodeModuleKind::Esm) } fn get_known_kind( &self, specifier: &Url, media_type: MediaType, ) -> Option { self.get_known_kind_with_is_script(specifier, media_type, None) } fn get_known_kind_with_is_script( &self, specifier: &Url, media_type: MediaType, is_script: Option, ) -> Option { self.is_cjs_resolver.get_known_kind_with_is_script( specifier, media_type, is_script, &self.known, ) } } #[derive(Debug)] pub struct IsCjsResolverOptions { pub detect_cjs: bool, pub is_node_main: bool, } /// Resolves whether a module is CJS or ESM. #[derive(Debug)] pub struct IsCjsResolver { in_npm_pkg_checker: Arc, pkg_json_resolver: Arc>, options: IsCjsResolverOptions, } impl IsCjsResolver { pub fn new( in_npm_pkg_checker: Arc, pkg_json_resolver: Arc>, options: IsCjsResolverOptions, ) -> Self { Self { in_npm_pkg_checker, pkg_json_resolver, options, } } /// Gets the referrer kind for a script in the LSP. pub fn get_lsp_referrer_kind( &self, specifier: &Url, is_script: Option, ) -> NodeModuleKind { if specifier.scheme() != "file" { return NodeModuleKind::Esm; } match MediaType::from_specifier(specifier) { MediaType::Mts | MediaType::Mjs | MediaType::Dmts => NodeModuleKind::Esm, MediaType::Cjs | MediaType::Cts | MediaType::Dcts => NodeModuleKind::Cjs, MediaType::Dts => { // dts files are always determined based on the package.json because // they contain imports/exports even when considered CJS self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm) } MediaType::Wasm | MediaType::Json => NodeModuleKind::Esm, MediaType::JavaScript | MediaType::Jsx | MediaType::TypeScript | MediaType::Tsx // treat these as unknown | MediaType::Css | MediaType::SourceMap | MediaType::Unknown => { match is_script { Some(true) => self.check_based_on_pkg_json(specifier).unwrap_or(NodeModuleKind::Esm), Some(false) | None => NodeModuleKind::Esm, } } } } fn get_known_kind_with_is_script( &self, specifier: &Url, media_type: MediaType, is_script: Option, known_cache: &DashMap, ) -> Option { if specifier.scheme() != "file" { return Some(NodeModuleKind::Esm); } match media_type { MediaType::Mts | MediaType::Mjs | MediaType::Dmts => Some(NodeModuleKind::Esm), MediaType::Cjs | MediaType::Cts | MediaType::Dcts => Some(NodeModuleKind::Cjs), MediaType::Dts => { // dts files are always determined based on the package.json because // they contain imports/exports even when considered CJS if let Some(value) = known_cache.get(specifier).map(|v| *v) { Some(value) } else { let value = self.check_based_on_pkg_json(specifier).ok(); if let Some(value) = value { known_cache.insert(specifier.clone(), value); } Some(value.unwrap_or(NodeModuleKind::Esm)) } } MediaType::Wasm | MediaType::Json => Some(NodeModuleKind::Esm), MediaType::JavaScript | MediaType::Jsx | MediaType::TypeScript | MediaType::Tsx // treat these as unknown | MediaType::Css | MediaType::SourceMap | MediaType::Unknown => { if let Some(value) = known_cache.get(specifier).map(|v| *v) { if value == NodeModuleKind::Cjs && is_script == Some(false) { // we now know this is actually esm known_cache.insert(specifier.clone(), NodeModuleKind::Esm); Some(NodeModuleKind::Esm) } else { Some(value) } } else if is_script == Some(false) { // we know this is esm known_cache.insert(specifier.clone(), NodeModuleKind::Esm); Some(NodeModuleKind::Esm) } else { None } } } } fn check_based_on_pkg_json( &self, specifier: &Url, ) -> Result { if self.in_npm_pkg_checker.in_npm_package(specifier) { if let Some(pkg_json) = self.pkg_json_resolver.get_closest_package_json(specifier)? { let is_file_location_cjs = pkg_json.typ != "module"; Ok(if is_file_location_cjs { NodeModuleKind::Cjs } else { NodeModuleKind::Esm }) } else { Ok(NodeModuleKind::Cjs) } } else if self.options.detect_cjs || self.options.is_node_main { if let Some(pkg_json) = self.pkg_json_resolver.get_closest_package_json(specifier)? { let is_cjs_type = pkg_json.typ == "commonjs" || self.options.is_node_main && pkg_json.typ == "none"; Ok(if is_cjs_type { NodeModuleKind::Cjs } else { NodeModuleKind::Esm }) } else if self.options.is_node_main { Ok(NodeModuleKind::Cjs) } else { Ok(NodeModuleKind::Esm) } } else { Ok(NodeModuleKind::Esm) } } }