#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum JsModuleKind {
Esm,
CommonJs,
Umd,
Amd,
System,
Iife,
}
impl JsModuleKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Esm => "esm",
Self::CommonJs => "commonjs",
Self::Umd => "umd",
Self::Amd => "amd",
Self::System => "system",
Self::Iife => "iife",
}
}
}
impl fmt::Display for JsModuleKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for JsModuleKind {
type Err = JsModuleKindParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(JsModuleKindParseError::Empty);
}
match trimmed.to_ascii_lowercase().as_str() {
"esm" | "esmodule" | "module" => Ok(Self::Esm),
"commonjs" | "cjs" => Ok(Self::CommonJs),
"umd" => Ok(Self::Umd),
"amd" => Ok(Self::Amd),
"system" | "systemjs" => Ok(Self::System),
"iife" => Ok(Self::Iife),
_ => Err(JsModuleKindParseError::Unknown),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JsModuleKindParseError {
Empty,
Unknown,
}
impl fmt::Display for JsModuleKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("module kind cannot be empty"),
Self::Unknown => formatter.write_str("unknown module kind"),
}
}
}
impl Error for JsModuleKindParseError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct JsModuleSpecifier(String);
impl JsModuleSpecifier {
pub fn new(input: &str) -> Result<Self, JsModuleSpecifierError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(JsModuleSpecifierError::Empty);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is_relative(&self) -> bool {
self.0.starts_with("./") || self.0.starts_with("../")
}
}
impl fmt::Display for JsModuleSpecifier {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for JsModuleSpecifier {
type Err = JsModuleSpecifierError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for JsModuleSpecifier {
type Error = JsModuleSpecifierError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JsModuleSpecifierError {
Empty,
}
impl fmt::Display for JsModuleSpecifierError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("module specifier cannot be empty")
}
}
impl Error for JsModuleSpecifierError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JsModuleFormat {
kind: JsModuleKind,
extension: Option<String>,
}
impl JsModuleFormat {
#[must_use]
pub const fn new(kind: JsModuleKind) -> Self {
Self {
kind,
extension: None,
}
}
#[must_use]
pub fn with_extension(mut self, extension: &str) -> Self {
let trimmed = extension.trim().trim_start_matches('.');
self.extension = (!trimmed.is_empty()).then(|| trimmed.to_string());
self
}
#[must_use]
pub const fn kind(&self) -> JsModuleKind {
self.kind
}
#[must_use]
pub fn extension(&self) -> Option<&str> {
self.extension.as_deref()
}
}
#[cfg(test)]
mod tests {
use super::{
JsModuleFormat, JsModuleKind, JsModuleKindParseError, JsModuleSpecifier,
JsModuleSpecifierError,
};
#[test]
fn parses_module_kinds() -> Result<(), JsModuleKindParseError> {
assert_eq!("esm".parse::<JsModuleKind>()?, JsModuleKind::Esm);
assert_eq!("cjs".parse::<JsModuleKind>()?, JsModuleKind::CommonJs);
assert_eq!(JsModuleKind::Iife.to_string(), "iife");
Ok(())
}
#[test]
fn validates_specifiers() -> Result<(), JsModuleSpecifierError> {
let specifier = JsModuleSpecifier::new(" ./app.js ")?;
assert_eq!(specifier.as_str(), "./app.js");
assert!(specifier.is_relative());
assert_eq!(
JsModuleSpecifier::new(" "),
Err(JsModuleSpecifierError::Empty)
);
Ok(())
}
#[test]
fn stores_format_metadata() {
let format = JsModuleFormat::new(JsModuleKind::Esm).with_extension(".mjs");
assert_eq!(format.kind(), JsModuleKind::Esm);
assert_eq!(format.extension(), Some("mjs"));
}
}