parcel_resolver/
specifier.rs

1use std::{
2  borrow::Cow,
3  path::{is_separator, Path, PathBuf},
4};
5
6use percent_encoding::percent_decode_str;
7
8use crate::{builtins::BUILTINS, url_to_path::url_to_path, Flags};
9
10/// Indicates how a specifier should be parsed.
11#[derive(Debug, PartialEq, Eq, Clone, Copy)]
12pub enum SpecifierType {
13  /// Parse the specifier as an ES module specifier.
14  /// This treats the specifier like a URL, but with support for bare module specifiers.
15  Esm,
16  /// Parse the specifier as a CommonJS specifier.
17  Cjs,
18  /// Parse the specifier as a URL.
19  /// Bare specifiers are treated like relative URLs.
20  Url,
21}
22
23/// An error that occurred while parsing a specifier.
24#[derive(Debug, Clone, PartialEq, serde::Serialize)]
25#[serde(tag = "kind", content = "value")]
26pub enum SpecifierError {
27  /// Specifier was an empty string.
28  EmptySpecifier,
29  /// Invalid specifier for an npm package.
30  InvalidPackageSpecifier,
31  /// Error parsing a URL.
32  #[serde(serialize_with = "serialize_url_error")]
33  UrlError(url::ParseError),
34  /// Invalid `file://` URL.
35  InvalidFileUrl,
36}
37
38impl From<url::ParseError> for SpecifierError {
39  fn from(value: url::ParseError) -> Self {
40    SpecifierError::UrlError(value)
41  }
42}
43
44fn serialize_url_error<S>(value: &url::ParseError, serializer: S) -> Result<S::Ok, S::Error>
45where
46  S: serde::Serializer,
47{
48  use serde::Serialize;
49  value.to_string().serialize(serializer)
50}
51
52/// Represents a module specifier.
53#[derive(PartialEq, Eq, Hash, Clone, Debug)]
54pub enum Specifier<'a> {
55  /// A relative specifier, e.g. './foo'.
56  Relative(Cow<'a, Path>),
57  /// An absolute specifier, e.g. '/foo/bar'.
58  Absolute(Cow<'a, Path>),
59  /// A tilde specifier, e.g. '~/foo'.
60  Tilde(Cow<'a, Path>),
61  /// A hash specifier, e.g. '#foo'.
62  Hash(Cow<'a, str>),
63  /// A package specifier and subpath, e.g. 'lodash/clone'.
64  Package(Cow<'a, str>, Cow<'a, str>),
65  /// A Node builtin module, e.g. 'path' or 'node:path'.
66  Builtin(Cow<'a, str>),
67  /// A URL specifier.
68  Url(&'a str),
69}
70
71impl<'a> Specifier<'a> {
72  /// Parses a specifier.
73  pub fn parse(
74    specifier: &'a str,
75    specifier_type: SpecifierType,
76    flags: Flags,
77  ) -> Result<(Specifier<'a>, Option<&'a str>), SpecifierError> {
78    if specifier.is_empty() {
79      return Err(SpecifierError::EmptySpecifier);
80    }
81
82    Ok(match specifier.as_bytes()[0] {
83      b'.' => {
84        let specifier = if let Some(specifier) = specifier.strip_prefix("./") {
85          specifier.trim_start_matches('/')
86        } else {
87          specifier
88        };
89        let (path, query) = decode_path(specifier, specifier_type);
90        (Specifier::Relative(path), query)
91      }
92      b'~' => {
93        let mut specifier = &specifier[1..];
94        if !specifier.is_empty() && is_separator(specifier.as_bytes()[0] as char) {
95          specifier = &specifier[1..];
96        }
97        let (path, query) = decode_path(specifier, specifier_type);
98        (Specifier::Tilde(path), query)
99      }
100      b'/' => {
101        if specifier.starts_with("//") && specifier_type == SpecifierType::Url {
102          // A protocol-relative URL, e.g `url('//example.com/foo.png')`.
103          (Specifier::Url(specifier), None)
104        } else {
105          let (path, query) = decode_path(specifier, specifier_type);
106          (Specifier::Absolute(path), query)
107        }
108      }
109      b'#' => (Specifier::Hash(Cow::Borrowed(&specifier[1..])), None),
110      _ => {
111        // Bare specifier.
112        match specifier_type {
113          SpecifierType::Url | SpecifierType::Esm => {
114            // Check if there is a scheme first.
115            if let Ok((scheme, rest)) = parse_scheme(specifier) {
116              let (path, rest) = parse_path(rest);
117              let (query, _) = parse_query(rest);
118              match scheme.as_ref() {
119                "npm" if flags.contains(Flags::NPM_SCHEME) => {
120                  if BUILTINS.contains(&path) {
121                    return Ok((Specifier::Builtin(Cow::Borrowed(path)), None));
122                  }
123
124                  (
125                    parse_package(percent_decode_str(path).decode_utf8_lossy())?,
126                    query,
127                  )
128                }
129                "node" => {
130                  // Node does not URL decode or support query params here.
131                  // See https://github.com/nodejs/node/issues/39710.
132                  (Specifier::Builtin(Cow::Borrowed(path)), None)
133                }
134                "file" => (
135                  Specifier::Absolute(Cow::Owned(url_to_path(specifier)?)),
136                  query,
137                ),
138                _ => (Specifier::Url(specifier), None),
139              }
140            } else {
141              // If not, then parse as an npm package if this is an ESM specifier,
142              // otherwise treat this as a relative path.
143              let (path, rest) = parse_path(specifier);
144              if specifier_type == SpecifierType::Esm {
145                if BUILTINS.contains(&path) {
146                  return Ok((Specifier::Builtin(Cow::Borrowed(path)), None));
147                }
148
149                let (query, _) = parse_query(rest);
150                (
151                  parse_package(percent_decode_str(path).decode_utf8_lossy())?,
152                  query,
153                )
154              } else {
155                let (path, query) = decode_path(specifier, specifier_type);
156                (Specifier::Relative(path), query)
157              }
158            }
159          }
160          SpecifierType::Cjs => {
161            if let Some(node_prefixed) = specifier.strip_prefix("node:") {
162              return Ok((Specifier::Builtin(Cow::Borrowed(node_prefixed)), None));
163            }
164
165            if BUILTINS.contains(&specifier) {
166              (Specifier::Builtin(Cow::Borrowed(specifier)), None)
167            } else {
168              #[cfg(windows)]
169              if !flags.contains(Flags::ABSOLUTE_SPECIFIERS) {
170                let path = Path::new(specifier);
171                if path.is_absolute() {
172                  return Ok((Specifier::Absolute(Cow::Borrowed(path)), None));
173                }
174              }
175
176              (parse_package(Cow::Borrowed(specifier))?, None)
177            }
178          }
179        }
180      }
181    })
182  }
183
184  /// Converts the specifier to a string.
185  pub fn to_string(&'a self) -> Cow<'a, str> {
186    match self {
187      Specifier::Relative(path) | Specifier::Absolute(path) | Specifier::Tilde(path) => {
188        path.as_os_str().to_string_lossy()
189      }
190      Specifier::Hash(path) => path.clone(),
191      Specifier::Package(module, subpath) => {
192        if subpath.is_empty() {
193          Cow::Borrowed(module)
194        } else {
195          let mut res = String::with_capacity(module.len() + subpath.len() + 1);
196          res.push_str(module);
197          res.push('/');
198          res.push_str(subpath);
199          Cow::Owned(res)
200        }
201      }
202      Specifier::Builtin(builtin) => Cow::Borrowed(builtin),
203      Specifier::Url(url) => Cow::Borrowed(url),
204    }
205  }
206}
207
208// https://url.spec.whatwg.org/#scheme-state
209// https://github.com/servo/rust-url/blob/1c1e406874b3d2aa6f36c5d2f3a5c2ea74af9efb/url/src/parser.rs#L387
210pub(crate) fn parse_scheme(input: &str) -> Result<(Cow<'_, str>, &str), ()> {
211  if input.is_empty() || !input.starts_with(ascii_alpha) {
212    return Err(());
213  }
214  let mut is_lowercase = true;
215  for (i, c) in input.chars().enumerate() {
216    match c {
217      'A'..='Z' => {
218        is_lowercase = false;
219      }
220      'a'..='z' | '0'..='9' | '+' | '-' | '.' => {}
221      ':' => {
222        let scheme = &input[0..i];
223        let rest = &input[i + 1..];
224        return Ok(if is_lowercase {
225          (Cow::Borrowed(scheme), rest)
226        } else {
227          (Cow::Owned(scheme.to_ascii_lowercase()), rest)
228        });
229      }
230      _ => {
231        return Err(());
232      }
233    }
234  }
235
236  // EOF before ':'
237  Err(())
238}
239
240// https://url.spec.whatwg.org/#path-state
241fn parse_path(input: &str) -> (&str, &str) {
242  // We don't really want to normalize the path (e.g. replacing ".." and "." segments).
243  // That is done later. For now, we just need to find the end of the path.
244  if let Some(pos) = input.chars().position(|c| c == '?' || c == '#') {
245    (&input[0..pos], &input[pos..])
246  } else {
247    (input, "")
248  }
249}
250
251// https://url.spec.whatwg.org/#query-state
252fn parse_query(input: &str) -> (Option<&str>, &str) {
253  if !input.is_empty() && input.as_bytes()[0] == b'?' {
254    if let Some(pos) = input.chars().position(|c| c == '#') {
255      (Some(&input[0..pos]), &input[pos..])
256    } else {
257      (Some(input), "")
258    }
259  } else {
260    (None, input)
261  }
262}
263
264/// https://url.spec.whatwg.org/#ascii-alpha
265#[inline]
266fn ascii_alpha(ch: char) -> bool {
267  ch.is_ascii_alphabetic()
268}
269
270fn parse_package(specifier: Cow<'_, str>) -> Result<Specifier, SpecifierError> {
271  match specifier {
272    Cow::Borrowed(specifier) => {
273      let (module, subpath) = parse_package_specifier(specifier)?;
274      Ok(Specifier::Package(
275        Cow::Borrowed(module),
276        Cow::Borrowed(subpath),
277      ))
278    }
279    Cow::Owned(specifier) => {
280      let (module, subpath) = parse_package_specifier(&specifier)?;
281      Ok(Specifier::Package(
282        Cow::Owned(module.to_owned()),
283        Cow::Owned(subpath.to_owned()),
284      ))
285    }
286  }
287}
288
289pub fn parse_package_specifier(specifier: &str) -> Result<(&str, &str), SpecifierError> {
290  let idx = specifier.chars().position(|p| p == '/');
291  if specifier.starts_with('@') {
292    let idx = idx.ok_or(SpecifierError::InvalidPackageSpecifier)?;
293    if let Some(next) = &specifier[idx + 1..].chars().position(|p| p == '/') {
294      Ok((
295        &specifier[0..idx + 1 + *next],
296        &specifier[idx + *next + 2..],
297      ))
298    } else {
299      Ok((specifier, ""))
300    }
301  } else if let Some(idx) = idx {
302    Ok((&specifier[0..idx], &specifier[idx + 1..]))
303  } else {
304    Ok((specifier, ""))
305  }
306}
307
308pub(crate) fn decode_path(
309  specifier: &str,
310  specifier_type: SpecifierType,
311) -> (Cow<'_, Path>, Option<&str>) {
312  match specifier_type {
313    SpecifierType::Url | SpecifierType::Esm => {
314      let (path, rest) = parse_path(specifier);
315      let (query, _) = parse_query(rest);
316      let path = match percent_decode_str(path).decode_utf8_lossy() {
317        Cow::Borrowed(v) => Cow::Borrowed(Path::new(v)),
318        Cow::Owned(v) => Cow::Owned(PathBuf::from(v)),
319      };
320      (path, query)
321    }
322    SpecifierType::Cjs => (Cow::Borrowed(Path::new(specifier)), None),
323  }
324}
325
326impl<'a> From<&'a str> for Specifier<'a> {
327  fn from(specifier: &'a str) -> Self {
328    Specifier::parse(specifier, SpecifierType::Cjs, Flags::empty())
329      .unwrap()
330      .0
331  }
332}
333
334impl<'de> serde::Deserialize<'de> for Specifier<'static> {
335  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
336  where
337    D: serde::Deserializer<'de>,
338  {
339    use serde::Deserialize;
340    let s: String = Deserialize::deserialize(deserializer)?;
341    // Specifiers are only deserialized as part of the "alias" and "browser" fields,
342    // so we assume CJS specifiers in Parcel mode.
343    Specifier::parse(&s, SpecifierType::Cjs, Flags::empty())
344      .map(|s| match s.0 {
345        Specifier::Relative(a) => Specifier::Relative(Cow::Owned(a.into_owned())),
346        Specifier::Absolute(a) => Specifier::Absolute(Cow::Owned(a.into_owned())),
347        Specifier::Tilde(a) => Specifier::Tilde(Cow::Owned(a.into_owned())),
348        Specifier::Hash(a) => Specifier::Hash(Cow::Owned(a.into_owned())),
349        Specifier::Package(a, b) => {
350          Specifier::Package(Cow::Owned(a.into_owned()), Cow::Owned(b.into_owned()))
351        }
352        Specifier::Builtin(a) => Specifier::Builtin(Cow::Owned(a.into_owned())),
353        Specifier::Url(_) => todo!(),
354      })
355      .map_err(|_| serde::de::Error::custom("Invalid specifier"))
356  }
357}