Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions crates/oxc_angular_compiler/src/component/cross_file_elision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ impl ImportValueResolver for CrossFileResolver {
fn resolve(&self, module: &str, name: &str) -> Option<crate::directive::StaticValue> {
self.inner.resolve_export_value(module, name, &self.from_file)
}

fn exports(
&self,
module: &str,
) -> Option<Vec<(String, Option<crate::directive::StaticValue>)>> {
self.inner.export_values_of(module, &self.from_file)
}
}

impl CrossFileAnalyzer {
Expand Down Expand Up @@ -341,6 +348,94 @@ impl Inner {
self.export_value_at(&resolved, import_name, 0)
}

/// Every value `import_source` exports, for `{ ...ns }` with
/// `import * as ns` (ngtsc's `ResolvedModule.getExports()`): each name
/// with the value it resolves to, or `None` for one that can't be read
/// statically — ngtsc keeps those as `DynamicValue`s in the object rather
/// than dropping the key. `None` for the whole map when the module can't
/// be resolved, like [`Self::resolve_export_value`].
fn export_values_of(
&self,
import_source: &str,
from_file: &Path,
) -> Option<Vec<(String, Option<crate::directive::StaticValue>)>> {
let parent = from_file.parent()?;
let resolved = self.resolver.resolve(parent, import_source).ok()?;
let resolved_path = resolved.full_path();
if resolved_path.components().any(|c| c.as_os_str() == "node_modules") {
return None;
}
let resolved = resolved_path.to_string_lossy().to_string();
self.value_dependencies.borrow_mut().insert(resolved.clone());
if !self.cache.borrow().contains_key(&resolved) {
if self.analyzing.borrow().contains(&resolved) {
return None; // Circular import
}
self.analyze_file(&resolved);
}
let mut names = Vec::new();
self.export_names_at(&resolved, &mut FxHashSet::default(), &mut names);
Some(
names
.into_iter()
.map(|name| {
let value = self.export_value_at(&resolved, &name, 0);
(name, value)
})
.collect(),
)
}

/// The names `file_path` exports, direct ones first — the first wins when
/// an `export *` re-exports a name the file also has (`getExportsOfModule`
/// answers one symbol per name) — then each star export's. `visited`
/// guards circular `export *` chains.
fn export_names_at(
&self,
file_path: &str,
visited: &mut FxHashSet<String>,
names: &mut Vec<String>,
) {
if !visited.insert(file_path.to_string()) {
return;
}
let Some(exports) = self.cache.borrow().get(file_path).cloned() else {
return;
};
// `export *` sources, stored under their `*:` keys (see
// `find_in_star_exports_inner`). `export * as ns` carries the same
// `(source, "*")` marker but exports `ns` as one name — it isn't
// enumerated flat.
let star_sources: Vec<String> = exports
.iter()
.filter(|(key, _)| key.starts_with("*:"))
.filter_map(|(_, info)| {
info.re_export_source.as_ref().map(|(source, _)| source.clone())
})
.collect();
for name in exports.keys() {
if !name.starts_with("*:") && !names.contains(name) {
names.push(name.clone());
}
}
let Some(file_parent) = Path::new(file_path).parent() else {
return;
};
for source in star_sources {
let Some(resolved) = self.resolve_module_spec(file_parent, &source) else {
continue;
};
self.value_dependencies.borrow_mut().insert(resolved.clone());
if !self.cache.borrow().contains_key(&resolved) {
if self.analyzing.borrow().contains(&resolved) {
continue;
}
self.analyze_file(&resolved);
}
self.export_names_at(&resolved, visited, names);
}
}

/// The value `export_name` binds to in `file_path`, direct or through
/// re-exports. Bounded by [`MAX_EXPORT_CHAIN`] for circular chains.
fn export_value_at(
Expand Down
126 changes: 64 additions & 62 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ use super::metadata::{
};
use super::transform::ImportMap;
use crate::directive::{
StringConsts, extract_host_bindings_in, extract_host_listeners_in, extract_input_metadata_in,
extract_output_metadata_in, merge_by_class_property, parse_decorator_io,
HostKey, StringConsts, extract_host_bindings_in, extract_host_listeners_in,
extract_input_metadata_in, extract_output_metadata_in, merge_by_class_property,
parse_decorator_io,
};
use crate::output::oxc_converter::convert_oxc_expression;
use crate::util::is_metadata_property;
Expand Down Expand Up @@ -106,13 +107,31 @@ pub fn extract_component_metadata<'a>(
crate::directive::extract_string_value(allocator, &prop.value, consts);
}
"styles" => {
if let Some(styles) = extract_string_array(allocator, &prop.value, consts) {
metadata.styles = styles;
} else if let Some(style) =
crate::directive::extract_string_value(allocator, &prop.value, consts)
{
// Single style string (legacy support)
metadata.styles.push(style);
// ngtsc's `reflectObjectLiteral` drops computed keys and
// folds duplicate `styles` entries with `Map.set` — the
// last one is the only one `parseDirectiveStyles` sees.
if prop.computed {
continue;
}
// Anything that isn't styles is an error `styles_error` reports.
match crate::directive::evaluate_styles(&prop.value, consts) {
Ok(styles) => {
metadata.styles = Vec::from_iter_in(
styles.iter().map(|style| Ident::from(allocator.alloc_str(style))),
&allocator,
);
// A later `styles:` overwrites an earlier failed
// one: `Map.set` makes the last entry the only
// one upstream reads.
metadata.styles_resolved = true;
}
Err(_) => {
// Failed evaluation is reported as a diagnostic;
// here it marks the value unknown so readers do
// not mistake the empty array for "no styles".
metadata.styles.clear();
metadata.styles_resolved = false;
}
}
}
"styleUrls" | "styleUrl" => {
Expand All @@ -137,7 +156,13 @@ pub fn extract_component_metadata<'a>(
metadata.change_detection = Some(extract_change_detection(&prop.value));
}
"host" => {
metadata.host = extract_host_metadata(allocator, &prop.value, consts);
// ngtsc's `reflectObjectLiteral` drops computed keys, and
// `host_metadata_error` doesn't see this property either —
// an invalid one must not be applied here where it would
// be dropped silently.
if !prop.computed {
metadata.host = extract_host_metadata(allocator, &prop.value, consts);
}
}
"imports" => {
// For standalone components, we need:
Expand Down Expand Up @@ -216,6 +241,13 @@ pub fn extract_component_metadata<'a>(

// Extract host bindings and listeners from @HostBinding/@HostListener decorators on class members
// These are merged with any host metadata from the @Component({ host: {} }) property
//
// Divergence from ngtsc (pre-existing merge structure): upstream assigns
// member entries into `bindings.properties[name]` / `bindings.listeners`
// [eventName], overwriting same-named `host:` entries so one binding per
// key remains. Here they are appended instead, so a key declared in both
// emits two bindings — both listeners fire, and both property ops emit
// (the member's, applied last, still wins like upstream).
let host_bindings = extract_host_bindings_in(allocator, class, Some(consts));
let host_listeners = extract_host_listeners_in(allocator, class, Some(consts));

Expand Down Expand Up @@ -480,71 +512,38 @@ fn extract_change_detection(expr: &Expression<'_>) -> ChangeDetectionStrategy {
}
}

/// Extract host metadata from a host object expression.
/// Extract host metadata from a host expression.
///
/// Reference: packages/compiler/src/render3/view/compiler.ts:560-604
fn extract_host_metadata<'a>(
allocator: &'a Allocator,
expr: &Expression<'a>,
expr: &'a Expression<'a>,
consts: &StringConsts<'a>,
) -> Option<HostMetadata<'a>> {
let Expression::ObjectExpression(obj) = expr else {
return None;
};

let mut host = HostMetadata {
properties: Vec::new_in(&allocator),
attributes: Vec::new_in(&allocator),
listeners: Vec::new_in(&allocator),
class_attr: None,
style_attr: None,
};

for prop in &obj.properties {
if let ObjectPropertyKind::ObjectProperty(prop) = prop
&& is_metadata_property(prop)
{
let Some(key_name) = get_property_key_name(&prop.key, consts) else {
continue;
};
let Some(value) =
crate::directive::extract_string_value(allocator, &prop.value, consts)
else {
continue;
};

let key_str = key_name.as_str();

if key_str.starts_with('[') && key_str.ends_with(']') {
// Property binding: [class.active]
host.properties.push((key_name, value));
} else if key_str.starts_with('(') && key_str.ends_with(')') {
// Event listener: (click)
host.listeners.push((key_name, value));
} else {
// Check for special attributes (class and style)
// Reference: compiler.ts:567-588
match key_str {
"class" => {
host.class_attr = Some(value);
}
"style" => {
host.style_attr = Some(value);
}
_ => {
// Regular static attribute
host.attributes.push((key_name, value));
}
}
}
// An unusable host is reported by `decorator_io_errors`.
let entries =
crate::directive::evaluate_host_metadata(allocator, expr, "Component", consts).ok()?;

let mut host = HostMetadata::new(allocator);
for (key, value) in entries {
match HostKey::of(key.as_str()) {
HostKey::Property => host.properties.push((key, value)),
HostKey::Listener => host.listeners.push((key, value)),
HostKey::Class => host.class_attr = Some(value),
HostKey::Style => host.style_attr = Some(value),
HostKey::Attribute => host.attributes.push((key, value)),
}
}

Some(host)
}

/// Extract host directives from a hostDirectives array expression.
///
/// Divergence from ngtsc's `extractHostDirectives`: upstream evaluates the
/// expression with the partial evaluator (a const or spread array works, a
/// non-array is an error). Here only a literal `ArrayExpression` is read and
/// anything else yields no host directives, with no diagnostic.
///
/// Handles the following patterns:
/// - Simple identifier: `hostDirectives: [TooltipDirective]`
/// - Object with directive: `hostDirectives: [{ directive: ColorDirective }]`
Expand Down Expand Up @@ -732,6 +731,9 @@ fn extract_forward_ref_directive_name<'a>(arg: Option<&Argument<'a>>) -> Option<
/// - Mapping string: `["color: bgColor"]` - public name mapped to internal name
///
/// Returns Vec of (publicName, internalName) pairs.
///
/// Divergence: ngtsc evaluates the array with the partial evaluator, so a
/// const or a spread works; here only a literal `ArrayExpression` is read.
fn extract_io_mappings<'a>(
allocator: &'a Allocator,
expr: &Expression<'a>,
Expand Down
7 changes: 7 additions & 0 deletions crates/oxc_angular_compiler/src/component/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ pub struct ComponentMetadata<'a> {
/// Inline styles array.
pub styles: Vec<'a, Ident<'a>>,

/// `false` when the decorator had a `styles` field the static evaluator
/// could not resolve (e.g. an import `resolveImportedValues` does not
/// cover): `styles` is then empty and must not be read as "no styles".
/// `true` when `styles` was absent or fully evaluated.
pub styles_resolved: bool,

/// URLs to external stylesheet files.
pub style_urls: Vec<'a, Ident<'a>>,

Expand Down Expand Up @@ -659,6 +665,7 @@ impl<'a> ComponentMetadata<'a> {
template: None,
template_url: None,
styles: Vec::new_in(&allocator),
styles_resolved: true,
style_urls: Vec::new_in(&allocator),
standalone: implicit_standalone,
encapsulation: ViewEncapsulation::default(),
Expand Down
Loading
Loading