mirror of
https://github.com/aphrody-labs/deno.git
synced 2026-10-14 06:19:28 +00:00
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`
231 lines
7.5 KiB
Rust
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)
|
|
}
|
|
}
|