use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use swc_common::comments::SingleThreadedComments;
use swc_common::sync::Lrc;
use swc_common::{FileName, SourceMap};
use swc_ecma_ast::Module;
use swc_ecma_parser::lexer::Lexer;
use swc_ecma_parser::{EsSyntax, Parser, StringInput, Syntax, TsSyntax};
pub struct ParsedModule {
pub path: PathBuf,
pub module: Module,
pub source_map: Lrc<SourceMap>,
pub comments: SingleThreadedComments,
}
#[derive(Debug, Clone)]
pub struct ParseFileError {
path: PathBuf,
details: String,
}
impl ParseFileError {
pub(crate) fn new(path: &Path, details: impl Into<String>) -> Self {
Self {
path: path.to_path_buf(),
details: details.into(),
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl fmt::Display for ParseFileError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.path.display(), self.details)
}
}
impl Error for ParseFileError {}
pub fn parse_file(path: &Path) -> Result<ParsedModule, ParseFileError> {
let source = std::fs::read_to_string(path)
.map_err(|error| ParseFileError::new(path, format!("failed to read file: {error}")))?;
parse_source(path, &source)
}
pub(crate) fn parse_source(path: &Path, source: &str) -> Result<ParsedModule, ParseFileError> {
let syntax = syntax_for_path(path)?;
let source_map: Lrc<SourceMap> = Lrc::new(SourceMap::default());
let comments = SingleThreadedComments::default();
let source_file =
source_map.new_source_file(FileName::Real(path.to_path_buf()).into(), source.to_owned());
let lexer = Lexer::new(
syntax,
Default::default(),
StringInput::from(&*source_file),
Some(&comments),
);
let mut parser = Parser::new_from(lexer);
let mut diagnostics = Vec::new();
for error in parser.take_errors() {
diagnostics.push(error.kind().msg().to_string());
}
let module = parser
.parse_module()
.map_err(|error| ParseFileError::new(path, error.kind().msg().to_string()))?;
for error in parser.take_errors() {
diagnostics.push(error.kind().msg().to_string());
}
if !diagnostics.is_empty() {
return Err(ParseFileError::new(path, diagnostics.join("; ")));
}
Ok(ParsedModule {
path: path.to_path_buf(),
module,
source_map,
comments,
})
}
fn syntax_for_path(path: &Path) -> Result<Syntax, ParseFileError> {
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.ok_or_else(|| ParseFileError::new(path, "missing file extension"))?;
let syntax = match extension {
"js" | "mjs" | "cjs" | "jsx" => Syntax::Es(EsSyntax {
jsx: true,
..Default::default()
}),
"ts" => Syntax::Typescript(TsSyntax {
tsx: false,
decorators: true,
..Default::default()
}),
"tsx" => Syntax::Typescript(TsSyntax {
tsx: true,
decorators: true,
..Default::default()
}),
_ => {
return Err(ParseFileError::new(
path,
format!("unsupported source extension `{extension}`"),
));
}
};
Ok(syntax)
}