Skip to content

Commit b712931

Browse files
committed
vite plugin
1 parent f5cc119 commit b712931

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+10301
-149
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ multiple_crate_versions = "allow"
105105
# publish = true
106106
oxc = { version = "0.105.0", path = "crates/oxc" } # Main entry point
107107
oxc_allocator = { version = "0.105.0", path = "crates/oxc_allocator" } # Memory management
108+
oxc_angular_compiler = { path = "crates/oxc_angular_compiler" } # Angular compiler
108109
oxc_ast = { version = "0.105.0", path = "crates/oxc_ast" } # AST definitions
109110
oxc_ast_macros = { version = "0.105.0", path = "crates/oxc_ast_macros" } # AST proc macros
110111
oxc_ast_visit = { version = "0.105.0", path = "crates/oxc_ast_visit" } # AST visitor pattern

crates/oxc_angular_compiler/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ doctest = false
1616

1717
[dependencies]
1818
oxc_allocator = { workspace = true }
19+
oxc_ast = { workspace = true }
20+
oxc_codegen = { workspace = true, features = ["sourcemap"] }
1921
oxc_diagnostics = { workspace = true }
22+
oxc_parser = { workspace = true }
2023
oxc_span = { workspace = true }
24+
miette = { workspace = true }
2125
rustc-hash = { workspace = true }
2226
lazy-regex = { workspace = true }
2327

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
//! Angular `@Component` decorator parser.
2+
//!
3+
//! This module extracts metadata from `@Component({...})` decorators
4+
//! on TypeScript class declarations.
5+
6+
use oxc_allocator::{Allocator, Vec};
7+
use oxc_ast::ast::{
8+
Argument, ArrayExpressionElement, Class, Decorator, Expression, ObjectPropertyKind, PropertyKey,
9+
};
10+
use oxc_span::Atom;
11+
12+
use super::metadata::{
13+
ChangeDetectionStrategy, ComponentMetadata, HostMetadata, ViewEncapsulation,
14+
};
15+
16+
/// Extract component metadata from a class with decorators.
17+
///
18+
/// Searches for a `@Component({...})` decorator and parses its properties.
19+
/// Returns `None` if no `@Component` decorator is found.
20+
///
21+
/// # Example
22+
///
23+
/// ```typescript
24+
/// @Component({
25+
/// selector: 'app-root',
26+
/// template: '<h1>Hello</h1>',
27+
/// standalone: true,
28+
/// })
29+
/// export class AppComponent {}
30+
/// ```
31+
pub fn extract_component_metadata<'a>(
32+
allocator: &'a Allocator,
33+
class: &'a Class<'a>,
34+
) -> Option<ComponentMetadata<'a>> {
35+
// Get the class name
36+
let class_name = class.id.as_ref()?.name.clone();
37+
let class_span = class.span;
38+
39+
// Find the @Component decorator
40+
let component_decorator = find_component_decorator(&class.decorators)?;
41+
42+
// Get the decorator call arguments
43+
let call_expr = match &component_decorator.expression {
44+
Expression::CallExpression(call) => call,
45+
_ => return None,
46+
};
47+
48+
// Verify it's calling 'Component'
49+
if !is_component_call(&call_expr.callee) {
50+
return None;
51+
}
52+
53+
// Get the first argument (the config object)
54+
let config_arg = call_expr.arguments.first()?;
55+
let config_obj = match config_arg {
56+
Argument::ObjectExpression(obj) => obj,
57+
_ => return None,
58+
};
59+
60+
// Create metadata with defaults
61+
let mut metadata = ComponentMetadata::new(allocator, class_name, class_span);
62+
63+
// Parse each property in the config object
64+
for prop in &config_obj.properties {
65+
if let ObjectPropertyKind::ObjectProperty(prop) = prop {
66+
let key_name = get_property_key_name(&prop.key)?;
67+
68+
match key_name.as_str() {
69+
"selector" => {
70+
metadata.selector = extract_string_value(&prop.value);
71+
}
72+
"template" => {
73+
metadata.template = extract_string_value(&prop.value);
74+
}
75+
"templateUrl" => {
76+
metadata.template_url = extract_string_value(&prop.value);
77+
}
78+
"styles" => {
79+
if let Some(styles) = extract_string_array(allocator, &prop.value) {
80+
metadata.styles = styles;
81+
} else if let Some(style) = extract_string_value(&prop.value) {
82+
// Single style string (legacy support)
83+
metadata.styles.push(style);
84+
}
85+
}
86+
"styleUrls" | "styleUrl" => {
87+
if let Some(urls) = extract_string_array(allocator, &prop.value) {
88+
metadata.style_urls = urls;
89+
} else if let Some(url) = extract_string_value(&prop.value) {
90+
metadata.style_urls.push(url);
91+
}
92+
}
93+
"standalone" => {
94+
metadata.standalone = extract_boolean_value(&prop.value).unwrap_or(false);
95+
}
96+
"encapsulation" => {
97+
metadata.encapsulation = extract_encapsulation(&prop.value);
98+
}
99+
"changeDetection" => {
100+
metadata.change_detection = extract_change_detection(&prop.value);
101+
}
102+
"host" => {
103+
metadata.host = extract_host_metadata(allocator, &prop.value);
104+
}
105+
"imports" => {
106+
// For standalone components - we just need the identifiers
107+
metadata.imports = extract_identifier_array(allocator, &prop.value);
108+
}
109+
"exportAs" => {
110+
metadata.export_as = extract_string_value(&prop.value);
111+
}
112+
"preserveWhitespaces" => {
113+
metadata.preserve_whitespaces =
114+
extract_boolean_value(&prop.value).unwrap_or(false);
115+
}
116+
_ => {
117+
// Unknown property - ignore
118+
}
119+
}
120+
}
121+
}
122+
123+
Some(metadata)
124+
}
125+
126+
/// Find the @Component decorator in a list of decorators.
127+
fn find_component_decorator<'a>(decorators: &'a [Decorator<'a>]) -> Option<&'a Decorator<'a>> {
128+
decorators.iter().find(|d| match &d.expression {
129+
Expression::CallExpression(call) => is_component_call(&call.callee),
130+
Expression::Identifier(id) => id.name == "Component",
131+
_ => false,
132+
})
133+
}
134+
135+
/// Check if a callee expression is a call to 'Component'.
136+
fn is_component_call(callee: &Expression<'_>) -> bool {
137+
match callee {
138+
Expression::Identifier(id) => id.name == "Component",
139+
// Handle namespaced imports like ng.Component or core.Component
140+
Expression::StaticMemberExpression(member) => {
141+
matches!(&member.property.name.as_str(), &"Component")
142+
}
143+
_ => false,
144+
}
145+
}
146+
147+
/// Get the name of a property key as a string.
148+
fn get_property_key_name<'a>(key: &PropertyKey<'a>) -> Option<Atom<'a>> {
149+
match key {
150+
PropertyKey::StaticIdentifier(id) => Some(id.name.clone()),
151+
PropertyKey::StringLiteral(lit) => Some(lit.value.clone()),
152+
_ => None,
153+
}
154+
}
155+
156+
/// Extract a string value from an expression.
157+
fn extract_string_value<'a>(expr: &Expression<'a>) -> Option<Atom<'a>> {
158+
match expr {
159+
Expression::StringLiteral(lit) => Some(lit.value.clone()),
160+
Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => {
161+
// Simple template literal with no expressions: `template string`
162+
tpl.quasis.first().map(|q| q.value.raw.clone())
163+
}
164+
_ => None,
165+
}
166+
}
167+
168+
/// Extract a boolean value from an expression.
169+
fn extract_boolean_value(expr: &Expression<'_>) -> Option<bool> {
170+
match expr {
171+
Expression::BooleanLiteral(lit) => Some(lit.value),
172+
_ => None,
173+
}
174+
}
175+
176+
/// Extract an array of strings from an expression.
177+
fn extract_string_array<'a>(
178+
allocator: &'a Allocator,
179+
expr: &Expression<'a>,
180+
) -> Option<Vec<'a, Atom<'a>>> {
181+
let Expression::ArrayExpression(arr) = expr else {
182+
return None;
183+
};
184+
185+
let mut result = Vec::new_in(allocator);
186+
for element in &arr.elements {
187+
if let ArrayExpressionElement::StringLiteral(lit) = element {
188+
result.push(lit.value.clone());
189+
} else if let ArrayExpressionElement::TemplateLiteral(tpl) = element {
190+
if tpl.expressions.is_empty() {
191+
if let Some(quasi) = tpl.quasis.first() {
192+
result.push(quasi.value.raw.clone());
193+
}
194+
}
195+
}
196+
}
197+
198+
Some(result)
199+
}
200+
201+
/// Extract an array of identifiers (for imports).
202+
fn extract_identifier_array<'a>(
203+
allocator: &'a Allocator,
204+
expr: &Expression<'a>,
205+
) -> Vec<'a, Atom<'a>> {
206+
let mut result = Vec::new_in(allocator);
207+
208+
let Expression::ArrayExpression(arr) = expr else {
209+
return result;
210+
};
211+
212+
for element in &arr.elements {
213+
match element {
214+
ArrayExpressionElement::Identifier(id) => {
215+
result.push(id.name.clone());
216+
}
217+
// Handle spread elements, etc. - for now just collect identifiers
218+
_ => {}
219+
}
220+
}
221+
222+
result
223+
}
224+
225+
/// Extract ViewEncapsulation from an expression.
226+
fn extract_encapsulation(expr: &Expression<'_>) -> ViewEncapsulation {
227+
// Look for patterns like:
228+
// - ViewEncapsulation.None
229+
// - ViewEncapsulation.Emulated
230+
// - ViewEncapsulation.ShadowDom
231+
// - 0, 2, 3 (numeric values)
232+
match expr {
233+
Expression::StaticMemberExpression(member) => match member.property.name.as_str() {
234+
"None" => ViewEncapsulation::None,
235+
"ShadowDom" => ViewEncapsulation::ShadowDom,
236+
"Emulated" => ViewEncapsulation::Emulated,
237+
_ => ViewEncapsulation::default(),
238+
},
239+
Expression::NumericLiteral(num) => {
240+
// Angular's numeric values: Emulated = 0, None = 2, ShadowDom = 3
241+
match num.value as i32 {
242+
0 => ViewEncapsulation::Emulated,
243+
2 => ViewEncapsulation::None,
244+
3 => ViewEncapsulation::ShadowDom,
245+
_ => ViewEncapsulation::default(),
246+
}
247+
}
248+
_ => ViewEncapsulation::default(),
249+
}
250+
}
251+
252+
/// Extract ChangeDetectionStrategy from an expression.
253+
fn extract_change_detection(expr: &Expression<'_>) -> ChangeDetectionStrategy {
254+
match expr {
255+
Expression::StaticMemberExpression(member) => match member.property.name.as_str() {
256+
"OnPush" => ChangeDetectionStrategy::OnPush,
257+
"Default" => ChangeDetectionStrategy::Default,
258+
_ => ChangeDetectionStrategy::default(),
259+
},
260+
Expression::NumericLiteral(num) => {
261+
// Angular's numeric values: Default = 0, OnPush = 1
262+
match num.value as i32 {
263+
1 => ChangeDetectionStrategy::OnPush,
264+
_ => ChangeDetectionStrategy::default(),
265+
}
266+
}
267+
_ => ChangeDetectionStrategy::default(),
268+
}
269+
}
270+
271+
/// Extract host metadata from a host object expression.
272+
fn extract_host_metadata<'a>(
273+
allocator: &'a Allocator,
274+
expr: &Expression<'a>,
275+
) -> Option<HostMetadata<'a>> {
276+
let Expression::ObjectExpression(obj) = expr else {
277+
return None;
278+
};
279+
280+
let mut host = HostMetadata {
281+
properties: Vec::new_in(allocator),
282+
attributes: Vec::new_in(allocator),
283+
listeners: Vec::new_in(allocator),
284+
};
285+
286+
for prop in &obj.properties {
287+
if let ObjectPropertyKind::ObjectProperty(prop) = prop {
288+
let Some(key_name) = get_property_key_name(&prop.key) else {
289+
continue;
290+
};
291+
let Some(value) = extract_string_value(&prop.value) else {
292+
continue;
293+
};
294+
295+
let key_str = key_name.as_str();
296+
297+
if key_str.starts_with('[') && key_str.ends_with(']') {
298+
// Property binding: [class.active]
299+
host.properties.push((key_name, value));
300+
} else if key_str.starts_with('(') && key_str.ends_with(')') {
301+
// Event listener: (click)
302+
host.listeners.push((key_name, value));
303+
} else {
304+
// Static attribute
305+
host.attributes.push((key_name, value));
306+
}
307+
}
308+
}
309+
310+
Some(host)
311+
}
312+
313+
#[cfg(test)]
314+
mod tests {
315+
// Tests will be added when oxc_ast is available as a dependency
316+
}

0 commit comments

Comments
 (0)