1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[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 #[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#[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
80pub struct JsModuleSpecifier(String);
81
82impl JsModuleSpecifier {
83 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 #[must_use]
98 pub fn as_str(&self) -> &str {
99 &self.0
100 }
101
102 #[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#[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#[derive(Clone, Debug, Eq, PartialEq)]
147pub struct JsModuleFormat {
148 kind: JsModuleKind,
149 extension: Option<String>,
150}
151
152impl JsModuleFormat {
153 #[must_use]
155 pub const fn new(kind: JsModuleKind) -> Self {
156 Self {
157 kind,
158 extension: None,
159 }
160 }
161
162 #[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 #[must_use]
172 pub const fn kind(&self) -> JsModuleKind {
173 self.kind
174 }
175
176 #[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}