1use std::borrow::Cow;
2use std::fmt;
3use std::io;
4use std::path::{Path, PathBuf};
5
6use path_clean::PathClean;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use thiserror::Error;
9
10const VIRTUAL_PREFIX: &str = "virtual:";
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct ModuleId(PathBuf);
20
21impl ModuleId {
22 pub fn new(path: impl AsRef<Path>) -> Result<Self, ModuleIdError> {
24 let path = path.as_ref();
25
26 if path.as_os_str().is_empty() {
27 return Err(ModuleIdError::EmptyPath);
28 }
29
30 if looks_like_virtual(path) {
31 return Ok(Self(normalize_virtual(path)));
32 }
33
34 let joined = if path.is_absolute() {
35 path.to_path_buf()
36 } else {
37 std::env::current_dir()
38 .map_err(|source| ModuleIdError::CurrentDir { source })?
39 .join(path)
40 };
41
42 let cleaned = joined.clean();
43
44 #[cfg(target_family = "wasm")]
46 {
47 Ok(Self(cleaned))
48 }
49
50 #[cfg(not(target_family = "wasm"))]
52 {
53 match std::fs::canonicalize(&cleaned) {
54 Ok(canonical) => Ok(Self(canonical)),
55 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Self(cleaned)),
56 Err(err) => Err(ModuleIdError::Canonicalization {
57 path: cleaned,
58 source: err,
59 }),
60 }
61 }
62 }
63
64 pub fn new_virtual(id: impl Into<String>) -> Self {
66 let id = id.into();
67
68 if id.is_empty() {
69 return Self(PathBuf::from(VIRTUAL_PREFIX));
70 }
71
72 let normalized = if id.starts_with(VIRTUAL_PREFIX) {
73 id
74 } else {
75 format!("{VIRTUAL_PREFIX}{id}")
76 };
77
78 Self(PathBuf::from(normalized))
79 }
80
81 pub fn as_path(&self) -> &Path {
83 &self.0
84 }
85
86 pub fn into_path(self) -> PathBuf {
88 self.0
89 }
90
91 pub fn is_virtual(&self) -> bool {
93 let text = self.path_string();
94 text.starts_with(VIRTUAL_PREFIX) || text.starts_with("rolldown:") || text.starts_with('\0')
95 }
96
97 pub fn path_string(&self) -> Cow<'_, str> {
99 self.0.to_string_lossy()
100 }
101
102 #[cfg(test)]
103 pub(crate) fn from_canonical_path(path: PathBuf) -> Self {
104 Self(path)
105 }
106
107 fn from_serialized_path(path: PathBuf) -> Self {
108 Self(path)
109 }
110}
111
112impl fmt::Display for ModuleId {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 write!(f, "{}", self.path_string())
115 }
116}
117
118impl Serialize for ModuleId {
119 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
120 where
121 S: Serializer,
122 {
123 serializer.serialize_str(&self.path_string())
124 }
125}
126
127impl<'de> Deserialize<'de> for ModuleId {
128 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
129 where
130 D: Deserializer<'de>,
131 {
132 let value = String::deserialize(deserializer)?;
133
134 if value.starts_with(VIRTUAL_PREFIX) {
135 Ok(ModuleId::new_virtual(value))
136 } else {
137 Ok(ModuleId::from_serialized_path(PathBuf::from(value)))
138 }
139 }
140}
141
142#[derive(Debug, Error)]
144pub enum ModuleIdError {
145 #[error("module id path is empty")]
147 EmptyPath,
148
149 #[error("failed to resolve current directory: {source}")]
151 CurrentDir {
152 #[source]
153 source: io::Error,
154 },
155
156 #[error("failed to canonicalize path '{path}': {source}")]
158 Canonicalization {
159 path: PathBuf,
160 #[source]
161 source: io::Error,
162 },
163}
164
165fn looks_like_virtual(path: &Path) -> bool {
166 let text = path.to_string_lossy();
167 text.starts_with(VIRTUAL_PREFIX) || text.starts_with("rolldown:") || text.starts_with('\0')
168}
169
170fn normalize_virtual(path: &Path) -> PathBuf {
171 let text = path.to_string_lossy();
172 if text.starts_with(VIRTUAL_PREFIX) || text.starts_with("rolldown:") {
173 PathBuf::from(text.into_owned())
174 } else if text.starts_with('\0') {
175 let trimmed = text.trim_start_matches('\0');
176 PathBuf::from(format!("{VIRTUAL_PREFIX}{trimmed}"))
177 } else {
178 PathBuf::from(format!("{VIRTUAL_PREFIX}{text}"))
179 }
180}