Skip to main content

use_js_module/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// JavaScript module-system kind.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum JsModuleKind {
10    Esm,
11    CommonJs,
12    Umd,
13    Amd,
14    System,
15    Iife,
16}
17
18impl JsModuleKind {
19    /// Returns the lowercase module kind label.
20    #[must_use]
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::Esm => "esm",
24            Self::CommonJs => "commonjs",
25            Self::Umd => "umd",
26            Self::Amd => "amd",
27            Self::System => "system",
28            Self::Iife => "iife",
29        }
30    }
31}
32
33impl fmt::Display for JsModuleKind {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        formatter.write_str(self.as_str())
36    }
37}
38
39impl FromStr for JsModuleKind {
40    type Err = JsModuleKindParseError;
41
42    fn from_str(input: &str) -> Result<Self, Self::Err> {
43        let trimmed = input.trim();
44        if trimmed.is_empty() {
45            return Err(JsModuleKindParseError::Empty);
46        }
47
48        match trimmed.to_ascii_lowercase().as_str() {
49            "esm" | "esmodule" | "module" => Ok(Self::Esm),
50            "commonjs" | "cjs" => Ok(Self::CommonJs),
51            "umd" => Ok(Self::Umd),
52            "amd" => Ok(Self::Amd),
53            "system" | "systemjs" => Ok(Self::System),
54            "iife" => Ok(Self::Iife),
55            _ => Err(JsModuleKindParseError::Unknown),
56        }
57    }
58}
59
60/// Error returned when a module kind is not recognized.
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub enum JsModuleKindParseError {
63    Empty,
64    Unknown,
65}
66
67impl fmt::Display for JsModuleKindParseError {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        match self {
70            Self::Empty => formatter.write_str("module kind cannot be empty"),
71            Self::Unknown => formatter.write_str("unknown module kind"),
72        }
73    }
74}
75
76impl Error for JsModuleKindParseError {}
77
78/// Validated JavaScript module specifier.
79#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
80pub struct JsModuleSpecifier(String);
81
82impl JsModuleSpecifier {
83    /// Creates a non-empty module specifier.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`JsModuleSpecifierError::Empty`] when `input` is empty after trimming.
88    pub fn new(input: &str) -> Result<Self, JsModuleSpecifierError> {
89        let trimmed = input.trim();
90        if trimmed.is_empty() {
91            return Err(JsModuleSpecifierError::Empty);
92        }
93        Ok(Self(trimmed.to_string()))
94    }
95
96    /// Returns the specifier as a string slice.
97    #[must_use]
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101
102    /// Returns whether this specifier is relative.
103    #[must_use]
104    pub fn is_relative(&self) -> bool {
105        self.0.starts_with("./") || self.0.starts_with("../")
106    }
107}
108
109impl fmt::Display for JsModuleSpecifier {
110    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111        formatter.write_str(self.as_str())
112    }
113}
114
115impl FromStr for JsModuleSpecifier {
116    type Err = JsModuleSpecifierError;
117
118    fn from_str(input: &str) -> Result<Self, Self::Err> {
119        Self::new(input)
120    }
121}
122
123impl TryFrom<&str> for JsModuleSpecifier {
124    type Error = JsModuleSpecifierError;
125
126    fn try_from(value: &str) -> Result<Self, Self::Error> {
127        Self::new(value)
128    }
129}
130
131/// Error returned when a module specifier is invalid.
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub enum JsModuleSpecifierError {
134    Empty,
135}
136
137impl fmt::Display for JsModuleSpecifierError {
138    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139        formatter.write_str("module specifier cannot be empty")
140    }
141}
142
143impl Error for JsModuleSpecifierError {}
144
145/// Simple module format metadata.
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub struct JsModuleFormat {
148    kind: JsModuleKind,
149    extension: Option<String>,
150}
151
152impl JsModuleFormat {
153    /// Creates module format metadata for a kind.
154    #[must_use]
155    pub const fn new(kind: JsModuleKind) -> Self {
156        Self {
157            kind,
158            extension: None,
159        }
160    }
161
162    /// Adds a file extension label such as `mjs` or `cjs`.
163    #[must_use]
164    pub fn with_extension(mut self, extension: &str) -> Self {
165        let trimmed = extension.trim().trim_start_matches('.');
166        self.extension = (!trimmed.is_empty()).then(|| trimmed.to_string());
167        self
168    }
169
170    /// Returns the module kind.
171    #[must_use]
172    pub const fn kind(&self) -> JsModuleKind {
173        self.kind
174    }
175
176    /// Returns the optional extension label.
177    #[must_use]
178    pub fn extension(&self) -> Option<&str> {
179        self.extension.as_deref()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::{
186        JsModuleFormat, JsModuleKind, JsModuleKindParseError, JsModuleSpecifier,
187        JsModuleSpecifierError,
188    };
189
190    #[test]
191    fn parses_module_kinds() -> Result<(), JsModuleKindParseError> {
192        assert_eq!("esm".parse::<JsModuleKind>()?, JsModuleKind::Esm);
193        assert_eq!("cjs".parse::<JsModuleKind>()?, JsModuleKind::CommonJs);
194        assert_eq!(JsModuleKind::Iife.to_string(), "iife");
195        Ok(())
196    }
197
198    #[test]
199    fn validates_specifiers() -> Result<(), JsModuleSpecifierError> {
200        let specifier = JsModuleSpecifier::new(" ./app.js ")?;
201        assert_eq!(specifier.as_str(), "./app.js");
202        assert!(specifier.is_relative());
203        assert_eq!(
204            JsModuleSpecifier::new("  "),
205            Err(JsModuleSpecifierError::Empty)
206        );
207        Ok(())
208    }
209
210    #[test]
211    fn stores_format_metadata() {
212        let format = JsModuleFormat::new(JsModuleKind::Esm).with_extension(".mjs");
213        assert_eq!(format.kind(), JsModuleKind::Esm);
214        assert_eq!(format.extension(), Some("mjs"));
215    }
216}