/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/resolver/cjs/analyzer/mod.rs
473 строки
15 KB
Nathan Whitaker
fix(cjs): use loader sources for recursive analysis (#36111)
30 июл 2026, 23:13
Не верифицирован
30 июл 2026, 23:13
786225a
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::collections::BTreeMap; use deno_error::JsErrorBox; use deno_maybe_sync::MaybeDashMap; use deno_maybe_sync::MaybeSend; use deno_maybe_sync::MaybeSync; use deno_media_type::MediaType; use node_resolver::analyze::CjsAnalysis as ExtNodeCjsAnalysis; use node_resolver::analyze::CjsAnalysisExports; use node_resolver::analyze::CjsCodeAnalyzer; use node_resolver::analyze::CjsMemberReExport; use node_resolver::analyze::EsmAnalysisMode; use serde::Deserialize; use serde::Serialize; use url::Url; use super::CjsTrackerRc; use crate::npm::DenoInNpmPackageChecker; #[cfg(feature = "deno_ast")] mod deno_ast; #[cfg(feature = "deno_ast")] pub use deno_ast::DenoAstModuleExportAnalyzer; #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct ModuleExportsAndReExports { pub exports: Vec<String>, pub reexports: Vec<String>, /// Re-exports that pin down a specific member of the inner module. /// Set when the analyzer detects a `module.exports = require("X").MEMBER` /// shape. Downstream analysis filters the inner module's exports to /// those statically attached to `MEMBER` rather than treating the /// whole inner module as the re-export source. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub member_reexports: Vec<MemberReExport>, } #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MemberReExport { pub specifier: String, pub member: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DenoCjsAnalysis { /// The module was found to be an ES module. Esm, /// The module was found to be an ES module and /// it was analyzed for imports and exports. EsmAnalysis(ModuleExportsAndReExports), /// The module was CJS. Cjs(ModuleExportsAndReExports), } #[derive(Debug, Copy, Clone)] pub struct NodeAnalysisCacheSourceHash(pub u64); #[allow(clippy::disallowed_types, reason = "definition")] pub type NodeAnalysisCacheRc = deno_maybe_sync::MaybeArc<dyn NodeAnalysisCache>; pub trait NodeAnalysisCache: MaybeSend + MaybeSync { fn compute_source_hash(&self, source: &str) -> NodeAnalysisCacheSourceHash; fn get_cjs_analysis( &self, specifier: &Url, source_hash: NodeAnalysisCacheSourceHash, ) -> Option<DenoCjsAnalysis>; fn set_cjs_analysis( &self, specifier: &Url, source_hash: NodeAnalysisCacheSourceHash, analysis: &DenoCjsAnalysis, ); } pub struct NullNodeAnalysisCache; impl NodeAnalysisCache for NullNodeAnalysisCache { fn compute_source_hash(&self, _source: &str) -> NodeAnalysisCacheSourceHash { NodeAnalysisCacheSourceHash(0) } fn get_cjs_analysis( &self, _specifier: &Url, _source_hash: NodeAnalysisCacheSourceHash, ) -> Option<DenoCjsAnalysis> { None } fn set_cjs_analysis( &self, _specifier: &Url, _source_hash: NodeAnalysisCacheSourceHash, _analysis: &DenoCjsAnalysis, ) { } } #[sys_traits::auto_impl] pub trait DenoCjsCodeAnalyzerSys: sys_traits::FsRead + sys_traits::FsMetadata + MaybeSend + MaybeSync + 'static { } pub trait ModuleForExportAnalysis { fn specifier(&self) -> &Url; fn compute_is_script(&self) -> bool; fn analyze_cjs(&self) -> ModuleExportsAndReExports; fn analyze_es_runtime_exports(&self) -> ModuleExportsAndReExports; /// For every top-level `exports.MEMBER = IDENT` whose IDENT also /// receives static `IDENT.X = …` assignments at the top level, return /// the map from MEMBER to those X names. Used by the /// `module.exports = require(X).MEMBER` wrapper shape to narrow the /// inner module's advertised names to those statically attached to /// MEMBER. Built in a single walk of the top-level statements. fn analyze_member_export_props(&self) -> BTreeMap<String, Vec<String>>; } #[allow(clippy::disallowed_types, reason = "definition")] pub type ModuleExportAnalyzerRc = deno_maybe_sync::MaybeArc<dyn ModuleExportAnalyzer>; #[allow( clippy::disallowed_types, reason = "source text is always stored as Arc<str>" )] type ArcStr = std::sync::Arc<str>; pub trait ModuleExportAnalyzer: MaybeSend + MaybeSync { fn parse_module( &self, specifier: Url, media_type: MediaType, source: ArcStr, ) -> Result<Box<dyn ModuleForExportAnalysis>, JsErrorBox>; } /// A module export analyzer that will error when parsing a module. pub struct NotImplementedModuleExportAnalyzer; impl ModuleExportAnalyzer for NotImplementedModuleExportAnalyzer { fn parse_module( &self, _specifier: Url, _media_type: MediaType, _source: ArcStr, ) -> Result<Box<dyn ModuleForExportAnalysis>, JsErrorBox> { panic!("Enable the deno_ast feature to get module export analysis."); } } #[allow(clippy::disallowed_types, reason = "definition")] pub type DenoCjsCodeAnalyzerRc<TSys> = deno_maybe_sync::MaybeArc<DenoCjsCodeAnalyzer<TSys>>; type MemberPropsMap = BTreeMap<String, Vec<String>>; #[allow(clippy::disallowed_types, reason = "definition")] type MemberPropsCache = deno_maybe_sync::MaybeArc< MaybeDashMap<(Url, u64), deno_maybe_sync::MaybeArc<MemberPropsMap>>, >; /// Whether a source should skip CJS export analysis (treated as ESM) instead of /// being parsed for `module.exports`/`require`. /// /// Non-script files can't carry CJS exports. The file extension answers this for /// everything except extensionless files (`MediaType::Unknown`), which may be /// real modules (an npm `"main"` with no extension — see /// `test-module-main-extension-lookup`) OR binary assets a framework happened to /// `require()`. Feeding binary to swc panics (it asserts on a backwards span), so /// for `Unknown` we only analyze when the source looks like text rather than /// blanket-skipping every extensionless module. /// /// `MediaType::Json` is handled by the caller before this and is not considered /// here. fn should_skip_cjs_analysis(media_type: MediaType, source: &str) -> bool { let is_definitely_non_script = !matches!( media_type, MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs | MediaType::Jsx | MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Tsx | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Unknown ); let looks_binary = source.contains('\0') || source.contains('\u{FFFD}'); is_definitely_non_script || (media_type == MediaType::Unknown && looks_binary) } pub struct DenoCjsCodeAnalyzer<TSys: DenoCjsCodeAnalyzerSys> { cache: NodeAnalysisCacheRc, cjs_tracker: CjsTrackerRc<DenoInNpmPackageChecker, TSys>, module_export_analyzer: ModuleExportAnalyzerRc, /// Memoizes the per-member property map keyed by `(specifier, /// source_hash)`. Built once per inner module so that repeated /// `analyze_cjs_member_props` calls (different members on the same /// module, or the same lookup across multiple importers) do not /// re-parse the source. member_props_cache: MemberPropsCache, } impl<TSys: DenoCjsCodeAnalyzerSys> DenoCjsCodeAnalyzer<TSys> { pub fn new( cache: NodeAnalysisCacheRc, cjs_tracker: CjsTrackerRc<DenoInNpmPackageChecker, TSys>, module_export_analyzer: ModuleExportAnalyzerRc, ) -> Self { Self { cache, cjs_tracker, module_export_analyzer, member_props_cache: Default::default(), } } async fn inner_cjs_analysis( &self, specifier: &Url, source: &str, esm_analysis_mode: EsmAnalysisMode, ) -> Result<DenoCjsAnalysis, JsErrorBox> { let source = source.strip_prefix('\u{FEFF}').unwrap_or(source); // strip BOM let source_hash = self.cache.compute_source_hash(source); if let Some(analysis) = self.cache.get_cjs_analysis(specifier, source_hash) { return Ok(analysis); } let media_type = MediaType::from_specifier(specifier); if media_type == MediaType::Json { return Ok(DenoCjsAnalysis::Cjs(Default::default())); } if should_skip_cjs_analysis(media_type, source) { return Ok(DenoCjsAnalysis::Esm); } let cjs_tracker = self.cjs_tracker.clone(); let is_maybe_cjs = cjs_tracker .is_maybe_cjs(specifier, media_type) .map_err(JsErrorBox::from_err)?; let analysis = if is_maybe_cjs || esm_analysis_mode == EsmAnalysisMode::SourceImportsAndExports { let module_export_analyzer = self.module_export_analyzer.clone(); let analyze = { let specifier = specifier.clone(); let source: ArcStr = source.into(); move || -> Result<_, JsErrorBox> { let parsed_source = module_export_analyzer .parse_module(specifier, media_type, source)?; let is_script = is_maybe_cjs && parsed_source.compute_is_script(); let is_cjs = is_maybe_cjs && cjs_tracker .is_cjs_with_known_is_script( parsed_source.specifier(), media_type, is_script, ) .map_err(JsErrorBox::from_err)?; if is_cjs { let analysis = parsed_source.analyze_cjs(); Ok(DenoCjsAnalysis::Cjs(analysis)) } else { match esm_analysis_mode { EsmAnalysisMode::SourceOnly => Ok(DenoCjsAnalysis::Esm), EsmAnalysisMode::SourceImportsAndExports => { Ok(DenoCjsAnalysis::EsmAnalysis( parsed_source.analyze_es_runtime_exports(), )) } } } } }; #[cfg(feature = "sync")] { crate::rt::spawn_blocking(analyze).await.unwrap()? } #[cfg(not(feature = "sync"))] analyze()? } else { DenoCjsAnalysis::Esm }; self .cache .set_cjs_analysis(specifier, source_hash, &analysis); Ok(analysis) } } #[async_trait::async_trait(?Send)] impl<TSys: DenoCjsCodeAnalyzerSys> CjsCodeAnalyzer for DenoCjsCodeAnalyzer<TSys> { async fn analyze_cjs<'a>( &self, specifier: &Url, source: Option<Cow<'a, str>>, esm_analysis_mode: EsmAnalysisMode, ) -> Result<ExtNodeCjsAnalysis<'a>, JsErrorBox> { let source = match source { Some(source) => source, // The analyzer may be asked to recursively inspect CJS re-exports // after only resolving the target specifier. Do not load that target // independently here because it may not match the source selected by // the caller's module loader. Callers should pass loader-owned source // explicitly when recursive analysis is available. None => return Ok(ExtNodeCjsAnalysis::Cjs(empty_cjs_analysis())), }; let analysis = self .inner_cjs_analysis(specifier, &source, esm_analysis_mode) .await?; match analysis { DenoCjsAnalysis::Esm => Ok(ExtNodeCjsAnalysis::Esm(source, None)), DenoCjsAnalysis::EsmAnalysis(analysis) => Ok(ExtNodeCjsAnalysis::Esm( source, Some(to_ext_cjs_analysis_exports(analysis)), )), DenoCjsAnalysis::Cjs(analysis) => Ok(ExtNodeCjsAnalysis::Cjs( to_ext_cjs_analysis_exports(analysis), )), } } async fn analyze_cjs_member_props<'a>( &self, specifier: &Url, maybe_source: Option<Cow<'a, str>>, member: &str, ) -> Result<Option<Vec<String>>, JsErrorBox> { let source = match maybe_source { Some(source) => source, // See `analyze_cjs`: callers must provide loader-owned source. Without // it, report that the member could not be statically narrowed. None => return Ok(None), }; let source = source.strip_prefix('\u{FEFF}').unwrap_or(&source); let media_type = MediaType::from_specifier(specifier); if media_type == MediaType::Json { return Ok(None); } let source_hash = self.cache.compute_source_hash(source).0; let cache_key = (specifier.clone(), source_hash); if let Some(map) = self .member_props_cache .get(&cache_key) .map(|entry| entry.clone()) { return Ok(map.get(member).cloned()); } let module_export_analyzer = self.module_export_analyzer.clone(); let parse_specifier = specifier.clone(); let source_arc: ArcStr = source.into(); let analyze = move || -> Result<MemberPropsMap, JsErrorBox> { let parsed = module_export_analyzer.parse_module( parse_specifier, media_type, source_arc, )?; Ok(parsed.analyze_member_export_props()) }; #[cfg(feature = "sync")] let map = crate::rt::spawn_blocking(analyze).await.unwrap()?; #[cfg(not(feature = "sync"))] let map = analyze()?; #[allow(clippy::disallowed_types, reason = "definition")] let map = deno_maybe_sync::MaybeArc::new(map); let result = map.get(member).cloned(); self.member_props_cache.insert(cache_key, map); Ok(result) } } fn to_ext_cjs_analysis_exports( analysis: ModuleExportsAndReExports, ) -> CjsAnalysisExports { CjsAnalysisExports { exports: analysis.exports, reexports: analysis.reexports, member_reexports: analysis .member_reexports .into_iter() .map(|m| CjsMemberReExport { specifier: m.specifier, member: m.member, }) .collect(), } } fn empty_cjs_analysis() -> CjsAnalysisExports { CjsAnalysisExports { exports: vec![], reexports: vec![], member_reexports: vec![], } } #[cfg(test)] mod tests { use super::*; #[test] fn skips_non_script_media_types() { // Definitely-non-script extensions never carry CJS exports, regardless of // whether their bytes happen to look like text. for mt in [MediaType::Wasm, MediaType::Css, MediaType::Html] { assert!(should_skip_cjs_analysis(mt, "anything")); } } #[test] fn analyzes_script_media_types() { // Real script extensions are always analyzed, even if the source contains // bytes the binary heuristic would flag (the extension is authoritative). for mt in [ MediaType::JavaScript, MediaType::Mjs, MediaType::Cjs, MediaType::Jsx, MediaType::TypeScript, MediaType::Mts, MediaType::Cts, MediaType::Tsx, ] { assert!(!should_skip_cjs_analysis(mt, "module.exports = 1;")); assert!(!should_skip_cjs_analysis(mt, "still\u{FFFD}script")); } } #[test] fn analyzes_extensionless_text() { // Extensionless npm `"main"` entries (e.g. `test-module-main-extension-lookup`) // are real modules and must be analyzed rather than skipped. assert!(!should_skip_cjs_analysis( MediaType::Unknown, "module.exports = function () {};\n", )); assert!(!should_skip_cjs_analysis(MediaType::Unknown, "")); } #[test] fn skips_extensionless_binary() { // Extensionless binary assets a framework happened to `require()` must be // skipped: feeding them to swc panics on a backwards span. Detected via a // NUL byte or the U+FFFD replacement char (invalid UTF-8 read lossily). assert!(should_skip_cjs_analysis( MediaType::Unknown, "MZ\u{0}\u{0}binary", )); assert!(should_skip_cjs_analysis( MediaType::Unknown, "PNG\u{FFFD}\u{FFFD}", )); } }