1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use anyhow::Context;
7use arcstr::ArcStr;
8use dashmap::DashMap;
9use oxc_resolver::{
10 ModuleType, PackageJson as OxcPackageJson, PackageType, Resolution, ResolveError,
11 ResolverGeneric, TsConfig as OxcTsConfig,
12};
13use rolldown_common::{
14 ImportKind, ModuleDefFormat, ModuleId, PackageJson, Platform, ResolveOptions, ResolvedId,
15 TsConfig,
16};
17use rolldown_fs::{FileSystem, OsFileSystem};
18use rolldown_utils::dashmap::FxDashMap;
19use sugar_path::SugarPath as _;
20
21use crate::resolver_config::ResolverConfig;
22
23#[derive(Debug)]
24#[expect(clippy::struct_field_names)]
25pub struct Resolver<Fs: FileSystem = OsFileSystem> {
26 fs: Fs,
27 cwd: PathBuf,
28 default_resolver: ResolverGeneric<Fs>,
29 import_resolver: ResolverGeneric<Fs>,
31 require_resolver: ResolverGeneric<Fs>,
33 css_resolver: ResolverGeneric<Fs>,
35 new_url_resolver: ResolverGeneric<Fs>,
37 package_json_cache: FxDashMap<PathBuf, Arc<PackageJson>>,
38}
39
40impl<Fs: FileSystem + Clone + 'static> Resolver<Fs> {
41 pub fn new(
43 fs: Fs,
44 cwd: PathBuf,
45 platform: Platform,
46 tsconfig: &TsConfig,
47 resolve_options: ResolveOptions,
48 ) -> Self {
49 let config = ResolverConfig::build(&cwd, platform, tsconfig, resolve_options);
50
51 let default_resolver =
52 ResolverGeneric::new_with_file_system(fs.clone(), config.default_options);
53 let import_resolver = default_resolver.clone_with_options(config.import_options);
54 let require_resolver = default_resolver.clone_with_options(config.require_options);
55 let css_resolver = default_resolver.clone_with_options(config.css_options);
56 let new_url_resolver = default_resolver.clone_with_options(config.new_url_options);
57
58 Self {
59 fs,
60 cwd,
61 default_resolver,
62 import_resolver,
63 require_resolver,
64 css_resolver,
65 new_url_resolver,
66 package_json_cache: DashMap::default(),
67 }
68 }
69
70 pub fn clone_default_resolver(&self) -> ResolverGeneric<Fs> {
73 self.default_resolver.clone_with_options(self.default_resolver.options().clone())
74 }
75}
76
77#[derive(Debug)]
78pub struct ResolveReturn {
79 pub path: ArcStr,
80 pub module_def_format: ModuleDefFormat,
81 pub package_json: Option<Arc<PackageJson>>,
82}
83
84impl From<ResolveReturn> for ResolvedId {
85 fn from(resolved_return: ResolveReturn) -> Self {
86 ResolvedId {
87 id: ModuleId::new(resolved_return.path),
88 module_def_format: resolved_return.module_def_format,
89 package_json: resolved_return.package_json,
90 ..Default::default()
91 }
92 }
93}
94
95impl<Fs: FileSystem> Resolver<Fs> {
96 pub fn cwd(&self) -> &PathBuf {
97 &self.cwd
98 }
99
100 pub fn clear_cache(&self) {
101 self.default_resolver.clear_cache();
103 self.package_json_cache.clear();
104 }
105
106 pub fn resolve_tsconfig<T: AsRef<Path>>(
107 &self,
108 path: &T,
109 ) -> Result<Arc<OxcTsConfig>, ResolveError> {
110 self.default_resolver.resolve_tsconfig(path)
111 }
112
113 pub fn try_get_package_json_or_create(&self, path: &Path) -> anyhow::Result<Arc<PackageJson>> {
114 self
115 .inner_try_get_package_json_or_create(path)
116 .with_context(|| format!("Failed to read or parse package.json: {}", path.display()))
117 }
118
119 fn inner_try_get_package_json_or_create(&self, path: &Path) -> anyhow::Result<Arc<PackageJson>> {
120 if let Some(v) = self.package_json_cache.get(path) {
121 Ok(Arc::clone(v.value()))
122 } else {
123 let realpath = path.to_path_buf();
125 let json_bytes = self.fs.read(path)?;
126 let oxc_pkg_json = OxcPackageJson::parse(&self.fs, realpath.clone(), realpath, json_bytes)?;
127 let pkg_json = Arc::new(PackageJson::from_oxc_pkg_json(&oxc_pkg_json));
128 self.package_json_cache.insert(path.to_path_buf(), Arc::clone(&pkg_json));
129 Ok(pkg_json)
130 }
131 }
132
133 pub fn resolve(
144 &self,
145 importer: Option<&Path>,
146 specifier: &str,
147 import_kind: ImportKind,
148 is_user_defined_entry: bool,
149 ) -> Result<ResolveReturn, ResolveError> {
150 let selected_resolver = match import_kind {
151 ImportKind::Import | ImportKind::DynamicImport | ImportKind::HotAccept => {
152 &self.import_resolver
153 }
154 ImportKind::NewUrl => &self.new_url_resolver,
155 ImportKind::Require => &self.require_resolver,
156 ImportKind::AtImport | ImportKind::UrlImport => &self.css_resolver,
157 };
158
159 let mut resolution = if let Some(importer) = importer {
160 if importer.is_absolute() {
162 selected_resolver.resolve_file(importer, specifier)
163 } else if importer.to_str().is_some_and(|id| id.starts_with('\0')) {
164 selected_resolver.resolve_with_context(
166 self.cwd.as_path(),
167 specifier,
168 None,
169 &mut oxc_resolver::ResolveContext::default(),
170 )
171 } else {
172 selected_resolver.resolve_file(self.cwd.join(importer), specifier)
173 }
174 } else {
175 selected_resolver.resolve(self.cwd.as_path(), specifier)
176 };
177
178 if resolution.is_err() && (is_user_defined_entry || importer.is_none()) {
184 resolution =
185 self.try_rollup_compatibility_resolve(selected_resolver, importer, specifier, resolution);
186 }
187
188 resolution.map(|info| {
189 let package_json = info.package_json().map(|p| self.cached_package_json(p));
190 let module_def_format = infer_module_def_format(&info);
191 ResolveReturn {
192 path: info.full_path().to_str().expect("Should be valid utf8").into(),
193 module_def_format,
194 package_json,
195 }
196 })
197 }
198
199 fn cached_package_json(&self, oxc_pkg_json: &OxcPackageJson) -> Arc<PackageJson> {
200 Arc::clone(
201 self
202 .package_json_cache
203 .entry(oxc_pkg_json.realpath.clone())
204 .or_insert_with(|| Arc::new(PackageJson::from_oxc_pkg_json(oxc_pkg_json)))
205 .value(),
206 )
207 }
208
209 fn try_rollup_compatibility_resolve(
220 &self,
221 resolver: &ResolverGeneric<Fs>,
222 importer: Option<&Path>,
223 specifier: &str,
224 original_resolution: Result<Resolution, ResolveError>,
225 ) -> Result<Resolution, ResolveError> {
226 if specifier.starts_with('.') || specifier.starts_with('/') {
227 return original_resolution;
228 }
229
230 let importer_dir = importer
231 .and_then(|importer_path| importer_path.parent())
232 .and_then(|parent_dir| parent_dir.components().next().is_some().then_some(parent_dir))
233 .unwrap_or(self.cwd.as_path());
234
235 let joined = self.cwd.join(specifier);
237 let specifier_path = joined.normalize();
238 let fallback = resolver.resolve(importer_dir, &specifier_path.to_string_lossy());
239 if fallback.is_ok() { fallback } else { original_resolution }
240 }
241}
242
243fn infer_module_def_format(info: &Resolution) -> ModuleDefFormat {
246 let fmt = ModuleDefFormat::from_path(info.path());
247 if !matches!(fmt, ModuleDefFormat::Unknown) {
248 return fmt;
249 }
250 let is_js_like_extension = info
251 .path()
252 .extension()
253 .is_some_and(|ext| matches!(ext.to_str(), Some("js" | "jsx" | "ts" | "tsx")));
254 if is_js_like_extension {
255 if let Some(module_type) = info.module_type() {
256 return match module_type {
257 ModuleType::CommonJs => ModuleDefFormat::CjsPackageJson,
258 ModuleType::Module => ModuleDefFormat::EsmPackageJson,
259 ModuleType::Json | ModuleType::Wasm | ModuleType::Addon => ModuleDefFormat::Unknown,
260 };
261 }
262 if let Some(ty) = info.package_json().and_then(|pkg_json| pkg_json.r#type()) {
268 return match ty {
269 PackageType::CommonJs => ModuleDefFormat::CjsPackageJson,
270 PackageType::Module => ModuleDefFormat::EsmPackageJson,
271 };
272 }
273 }
274 ModuleDefFormat::Unknown
275}