1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//! TypeScript and JavaScript parser using SWC
//!
//! Global invariants enforced:
//! - Deterministic parsing order
//! - Formatting, comments, and whitespace must not affect results
use anyhow::Result;
use swc_common::{sync::Lrc, FileName, SourceFile, SourceMap};
use swc_ecma_ast::{EsVersion, Module};
use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
/// Determine the appropriate syntax configuration based on file extension
fn syntax_for_file(filename: &str) -> Syntax {
// Check if this is a TypeScript file
if filename.ends_with(".tsx") || filename.ends_with(".mtsx") || filename.ends_with(".ctsx") {
// TypeScript with JSX (TSX)
Syntax::Typescript(swc_ecma_parser::TsSyntax {
tsx: true,
decorators: true, // Angular and other decorator-heavy frameworks
dts: false,
..Default::default()
})
} else if filename.ends_with(".ts") || filename.ends_with(".mts") || filename.ends_with(".cts")
{
// TypeScript without JSX
let is_dts = filename.ends_with(".d.ts");
Syntax::Typescript(swc_ecma_parser::TsSyntax {
tsx: false,
decorators: true, // Angular uses @Component, @Injectable, etc.
dts: is_dts,
..Default::default()
})
} else if filename.ends_with(".jsx")
|| filename.ends_with(".mjsx")
|| filename.ends_with(".cjsx")
{
// JavaScript with JSX
Syntax::Es(swc_ecma_parser::EsSyntax {
jsx: true,
decorators: false,
..Default::default()
})
} else {
// Plain JavaScript (.js, .mjs, .cjs) — jsx: true is a superset of plain JS
Syntax::Es(swc_ecma_parser::EsSyntax {
jsx: true, // React webpack projects put JSX in .js files by convention
decorators: false,
..Default::default()
})
}
}
/// Parse TypeScript, JavaScript, JSX, or TSX source code into an AST module
///
/// Automatically detects file type based on extension and uses appropriate parser configuration.
///
/// Supported file types:
/// - `.ts`, `.mts`, `.cts` - TypeScript
/// - `.tsx`, `.mtsx`, `.ctsx` - TypeScript with JSX
/// - `.js`, `.mjs`, `.cjs` - JavaScript
/// - `.jsx`, `.mjsx`, `.cjsx` - JavaScript with JSX
///
/// Returns an error if parse errors occur.
pub fn parse_source(src: &str, source_map: &Lrc<SourceMap>, filename: &str) -> Result<Module> {
// Determine syntax based on file extension
let syntax = syntax_for_file(filename);
// Create SourceFile for the source code
let source_file: Lrc<SourceFile> =
source_map.new_source_file(FileName::Custom(filename.into()).into(), src.to_string());
// Create StringInput from SourceFile
let input = StringInput::from(&*source_file);
// Create lexer with detected syntax
let lexer = Lexer::new(syntax, EsVersion::Es2022, input, None);
// Create parser
let mut parser = Parser::new_from(lexer);
// Parse module
parser.parse_module().map_err(|e| {
let error_msg = e.kind().msg();
anyhow::anyhow!("Parse error: {}", error_msg)
.context(format!("Failed to parse source file: {}", filename))
})
}
/// Legacy alias for backwards compatibility
///
/// Use `parse_source` instead for new code.
#[deprecated(since = "0.1.0", note = "Use parse_source instead")]
pub fn parse_typescript(src: &str, source_map: &Lrc<SourceMap>, filename: &str) -> Result<Module> {
parse_source(src, source_map, filename)
}
#[cfg(test)]
#[path = "parser/tests.rs"]
mod tests;