Files
Nathan Whitaker 786225adc1 fix(cjs): use loader sources for recursive analysis (#36111)
CJS export analysis recursively resolves re-exports, but it could reopen
those paths independently instead of using the source selected by the
active module loader. This could produce early parse failures and made
source handling differ across normal, npm, compiled, and standalone
loading paths.

This change:
- threads an explicit source provider through recursive export and
member analysis;
- supplies graph, npm, and VFS-backed sources from their owning loaders;
- uses each standalone loader's filesystem policy for runtime host
sources;
- falls back to incomplete static analysis when no loader-owned source
is available, leaving runtime resolution to handle the target.

The standalone path keeps permitted host-filesystem CJS re-exports
working, including named exports, while leaving interactive access
decisions to runtime execution.

Validation:
- `./tools/format.js`
- `./tools/lint.js`
- `cargo check -p node_resolver -p deno_resolver -p denort -p deno`
- `cargo build --bin deno --bin denort`
- focused standard and standalone analyzer regressions
- existing CJS re-export, require-to-ESM, import, and standalone
fallback specs
- `cargo test -p node_resolver`
2026-07-30 13:13:25 -07:00

231 lines
7.5 KiB
Rust

// Copyright 2018-2026 the Deno authors. MIT license.
use std::borrow::Cow;
use std::sync::Arc;
use deno_core::url::Url;
use deno_error::JsErrorBox;
use deno_lib::standalone::binary::CjsExportAnalysisEntry;
use deno_media_type::MediaType;
use deno_resolver::loader::NpmModuleLoader;
use deno_resolver::npm::DenoInNpmPackageChecker;
use deno_resolver::npm::NpmReqResolver;
use node_resolver::DenoIsBuiltInNodeModuleChecker;
use node_resolver::analyze::CjsAnalysis;
use node_resolver::analyze::CjsAnalysisExports;
use node_resolver::analyze::EsmAnalysisMode;
use node_resolver::analyze::NodeCodeTranslator;
use crate::binary::StandaloneModules;
use crate::file_system::DenoRtSys;
pub type DenoRtCjsTracker =
deno_resolver::cjs::CjsTracker<DenoInNpmPackageChecker, DenoRtSys>;
pub type DenoRtNpmResolver = deno_resolver::npm::NpmResolver<DenoRtSys>;
pub type DenoRtNpmModuleLoader = NpmModuleLoader<
CjsCodeAnalyzer,
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
DenoRtNpmResolver,
DenoRtSys,
>;
pub type DenoRtNodeCodeTranslator = NodeCodeTranslator<
CjsCodeAnalyzer,
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
DenoRtNpmResolver,
DenoRtSys,
>;
pub type DenoRtNodeResolver = deno_runtime::deno_node::NodeResolver<
DenoInNpmPackageChecker,
DenoRtNpmResolver,
DenoRtSys,
>;
pub type DenoRtNpmReqResolver = NpmReqResolver<
DenoInNpmPackageChecker,
DenoIsBuiltInNodeModuleChecker,
DenoRtNpmResolver,
DenoRtSys,
>;
pub struct CjsCodeAnalyzer {
cjs_tracker: Arc<DenoRtCjsTracker>,
modules: Arc<StandaloneModules>,
sys: DenoRtSys,
}
impl CjsCodeAnalyzer {
pub fn new(
cjs_tracker: Arc<DenoRtCjsTracker>,
modules: Arc<StandaloneModules>,
sys: DenoRtSys,
) -> Self {
Self {
cjs_tracker,
modules,
sys,
}
}
fn inner_cjs_analysis<'a>(
&self,
specifier: &Url,
source: Cow<'a, str>,
) -> Result<CjsAnalysis<'a>, JsErrorBox> {
let media_type = MediaType::from_specifier(specifier);
if media_type == MediaType::Json {
return Ok(CjsAnalysis::Cjs(CjsAnalysisExports {
exports: vec![],
reexports: vec![],
member_reexports: vec![],
}));
}
let cjs_tracker = &self.cjs_tracker;
let is_maybe_cjs = cjs_tracker
.is_maybe_cjs(specifier, media_type)
.map_err(JsErrorBox::from_err)?;
let analysis = if is_maybe_cjs {
let data = self
.modules
.read(specifier)?
.and_then(|d| d.cjs_export_analysis);
match data {
Some(data) => {
let data: CjsExportAnalysisEntry = bincode::deserialize(&data)
.map_err(|err| JsErrorBox::generic(err.to_string()))?;
match data {
CjsExportAnalysisEntry::Esm => {
cjs_tracker.set_is_known_script(specifier, false);
CjsAnalysis::Esm(source, None)
}
CjsExportAnalysisEntry::Cjs(exports) => {
cjs_tracker.set_is_known_script(specifier, true);
CjsAnalysis::Cjs(CjsAnalysisExports {
exports,
reexports: Vec::new(), // already resolved
member_reexports: Vec::new(),
})
}
CjsExportAnalysisEntry::Error(err) => {
return Err(JsErrorBox::generic(err));
}
}
}
None => {
if self.sys.is_specifier_in_vfs(specifier) {
// VFS-embedded modules always have a pre-computed analysis
// written by the compile step. A missing entry here means
// the compile step skipped analysis for this file — bug.
if log::log_enabled!(log::Level::Debug) {
log::debug!(
"No CJS export analysis was stored for '{}'. Assuming ESM. This might indicate a bug in Deno.",
specifier
);
}
CjsAnalysis::Esm(source, None)
} else {
// Host-FS fallback: the file was read from the user's real
// filesystem (HMR / dev mode), so it has no embedded
// analysis. Run the analyzer live with deno_ast — without
// this the loader would treat every host-FS CJS module as
// ESM and any `import { name }` from it would fail with
// "does not provide an export named 'name'", even for
// common npm packages whose CJS index re-exports names
// via `Object.defineProperty(exports, ...)`.
analyze_cjs_live(cjs_tracker, specifier, media_type, source)?
}
}
}
} else {
CjsAnalysis::Esm(source, None)
};
Ok(analysis)
}
}
/// Parse `source` with deno_ast and classify it as CJS (with its named
/// exports) or ESM. Used at runtime in the standalone executable for
/// files read from the host filesystem (HMR / dev mode) which have no
/// pre-computed analysis embedded in the VFS.
///
/// Mirrors what `DenoCjsCodeAnalyzer` does at compile time, minus the
/// re-export resolution (the loader walks reexports separately). The
/// `compute_is_script` + `is_cjs_with_known_is_script` two-step is the
/// same one the compile-time analyzer uses; without it, ESM files that
/// happen to look CJS-ish would be mis-classified.
fn analyze_cjs_live<'a>(
cjs_tracker: &DenoRtCjsTracker,
specifier: &Url,
media_type: MediaType,
source: Cow<'a, str>,
) -> Result<CjsAnalysis<'a>, JsErrorBox> {
let parsed = deno_ast::parse_program(deno_ast::ParseParams {
specifier: specifier.clone(),
text: source.to_string().into(),
media_type,
capture_tokens: true,
scope_analysis: false,
maybe_syntax: None,
})
.map_err(JsErrorBox::from_err)?;
let is_script = parsed.compute_is_script();
let is_cjs = cjs_tracker
.is_cjs_with_known_is_script(specifier, media_type, is_script)
.map_err(JsErrorBox::from_err)?;
if is_cjs {
let analysis = parsed.analyze_cjs();
cjs_tracker.set_is_known_script(specifier, true);
Ok(CjsAnalysis::Cjs(CjsAnalysisExports {
exports: analysis.exports,
reexports: analysis.reexports,
// `deno_ast::analyze_cjs` doesn't surface member-level
// reexports; the loader walks them via its own reexport
// resolution.
member_reexports: Vec::new(),
}))
} else {
cjs_tracker.set_is_known_script(specifier, false);
Ok(CjsAnalysis::Esm(source, None))
}
}
#[async_trait::async_trait(?Send)]
impl node_resolver::analyze::CjsCodeAnalyzer for CjsCodeAnalyzer {
async fn analyze_cjs<'a>(
&self,
specifier: &Url,
source: Option<Cow<'a, str>>,
_esm_analysis_mode: EsmAnalysisMode,
) -> Result<CjsAnalysis<'a>, JsErrorBox> {
let source = match source {
Some(source) => source,
// Recursive analysis must use the source selected by the active module
// loader. If no loader-owned source is available, leave the export set
// empty and let runtime resolution handle the target.
None => {
return Ok(CjsAnalysis::Cjs(CjsAnalysisExports {
exports: vec![],
reexports: vec![],
member_reexports: vec![],
}));
}
};
self.inner_cjs_analysis(specifier, source)
}
async fn analyze_cjs_member_props<'a>(
&self,
_specifier: &Url,
_maybe_source: Option<Cow<'a, str>>,
_member: &str,
) -> Result<Option<Vec<String>>, JsErrorBox> {
// The compiled runtime does not have access to swc; CJS export
// analysis is precomputed at compile time. Member-shape narrowing
// is therefore unavailable here, and the wrapper falls back to
// advertising only the default and module.exports.
Ok(None)
}
}