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
53 changes: 53 additions & 0 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3469,11 +3469,26 @@ pub fn transform_angular_file(
shared_pool_index,
&mut file_namespace_registry,
&import_map,
resolved_resources,
) {
Ok(compilation_result) => {
// Update the shared pool index for the next component
shared_pool_index = compilation_result.next_pool_index;

// Stylesheet `<link>`s in the template are watched
// files like decorator `styleUrls`, and a missing
// one is the same non-fatal diagnostic.
for style_url in &compilation_result.template_style_urls {
result.dependencies.push(style_url.clone());
}
for style_url in &compilation_result.missing_link_urls {
result.diagnostics.push(OxcDiagnostic::error(format!(
"Component '{}': style URL '{}' could not be resolved \
(COMPONENT_RESOURCE_NOT_FOUND)",
class_name, style_url
)));
}

let component_id = format!("{}@{}", path, class_name);

let component_decorator =
Expand Down Expand Up @@ -4622,6 +4637,16 @@ struct FullCompilationResult {
/// async metadata callback, and emitting them would leave dead
/// `import(...)` calls in the output.
has_defer_block: bool,

/// Resolvable-URL stylesheet `<link>`s the template's preparser collected
/// (upstream `_extractTemplateStyleUrls`). The caller records them as
/// watch dependencies like decorator `styleUrls`.
template_style_urls: Vec<String>,

/// Template link URLs missing from `resolved_resources` — non-fatal like
/// a missing decorator `styleUrl`: the caller surfaces
/// COMPONENT_RESOURCE_NOT_FOUND in the result diagnostics.
missing_link_urls: Vec<String>,
}

/// Compile a component template and generate ɵcmp/ɵfac definitions.
Expand Down Expand Up @@ -4687,6 +4712,10 @@ fn compile_component_partial<'a>(
next_pool_index: pool_starting_index,
ng_content_selectors: Vec::new(),
has_defer_block,
// Partial mode never parses the template; the linker collects link
// URLs at consumer build time.
template_style_urls: Vec::new(),
missing_link_urls: Vec::new(),
}
}

Expand All @@ -4702,6 +4731,7 @@ fn compile_component_full<'a>(
pool_starting_index: u32,
namespace_registry: &mut NamespaceRegistry<'a>,
import_map: &ImportMap<'a>,
resolved_resources: Option<&ResolvedResources>,
) -> Result<FullCompilationResult, Vec<OxcDiagnostic>> {
use oxc_allocator::FromIn;

Expand Down Expand Up @@ -4776,6 +4806,27 @@ fn compile_component_full<'a>(
let ng_content_selectors: Vec<String> =
r3_result.ng_content_selectors.iter().map(|s| s.to_string()).collect();

// Resolve stylesheet `<link>`s the preparser collected (`_extractTemplateStyleUrls`):
// they compile to styles like decorator `styleUrls` (handler.ts resolves both through
// `resourceLoader` and the link element is removed from the template). Upstream puts
// external styles before inline ones; the file's own ordering keeps URL contents after
// the decorator's inline styles, and the links land ahead of the `<style>` contents
// below, like upstream.
let mut missing_link_urls = Vec::new();
let template_style_urls: Vec<String> =
r3_result.style_urls.iter().map(|url| url.to_string()).collect();
if let Some(resources) = resolved_resources {
for style_url in &r3_result.style_urls {
if let Some(style_contents) = resources.styles.get(style_url.as_str()) {
for style in style_contents {
metadata.styles.push(Ident::from_in(style.as_str(), &allocator));
}
} else {
missing_link_urls.push(style_url.to_string());
}
}
}

// Merge inline template styles into component metadata
// These are styles from <style> tags directly in the template HTML
for style in r3_result.styles.iter() {
Expand Down Expand Up @@ -5110,6 +5161,8 @@ fn compile_component_full<'a>(
next_pool_index,
ng_content_selectors,
has_defer_block,
template_style_urls,
missing_link_urls,
})
}

Expand Down
48 changes: 35 additions & 13 deletions crates/oxc_angular_compiler/src/parser/html/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,8 +1063,13 @@ impl<'a> HtmlLexer<'a> {
// A `<` followed by whitespace or other non-tag characters is just text
if self.peek() == '<' {
let next = self.peek_at(1);
// Valid tag start: `/` (close tag), `!` (comment/doctype/cdata), or letter/underscore
if next == '/' || next == '!' || next.is_ascii_alphabetic() || next == '_' {
// Valid tag start: `/` (close tag), `!` (comment/doctype/cdata), or a letter.
// An underscore starts a name only for a selectorless component.
if next == '/'
|| next == '!'
|| next.is_ascii_alphabetic()
|| (next == '_' && self.selectorless_enabled)
{
self.scan_tag(start);
return;
}
Expand Down Expand Up @@ -1718,10 +1723,10 @@ impl<'a> HtmlLexer<'a> {
let content_type = get_html_tag_definition(&lower_name).get_content_type(ns_prefix);
match content_type {
TagContentType::RawText => {
self.scan_raw_text_with_tag_close(&lower_name, false);
self.scan_raw_text_with_tag_close(&prefix, &name, false);
}
TagContentType::EscapableRawText => {
self.scan_raw_text_with_tag_close(&lower_name, true);
self.scan_raw_text_with_tag_close(&prefix, &name, true);
}
TagContentType::Parsable => {
// Normal parsable content, no special handling needed
Expand Down Expand Up @@ -1757,7 +1762,18 @@ impl<'a> HtmlLexer<'a> {
/// Scans raw text content until the closing tag.
/// For RAW_TEXT (script/style): entities are NOT decoded.
/// For ESCAPABLE_RAW_TEXT (title/textarea): entities ARE decoded.
fn scan_raw_text_with_tag_close(&mut self, tag_name: &str, consume_entities: bool) {
///
/// `tag_name` is the opening tag's local name as written. The closing tag is
/// matched against it ignoring case — upstream uses `closingTagName =
/// parts[1]` for ordinary elements — and the close token is emitted with the
/// opening tag's `[prefix, name]` parts, so `</script>` closes the element
/// named `:svg:script`.
fn scan_raw_text_with_tag_close(
&mut self,
prefix: &str,
tag_name: &str,
consume_entities: bool,
) {
let token_type =
if consume_entities { HtmlTokenType::EscapableRawText } else { HtmlTokenType::RawText };

Expand All @@ -1783,12 +1799,12 @@ impl<'a> HtmlLexer<'a> {
{
self.advance();
}
let close_tag_name =
self.input[close_tag_start as usize..self.index as usize].to_lowercase();
let close_tag_name = &self.input[close_tag_start as usize..self.index as usize];
let closes_tag = close_tag_name.eq_ignore_ascii_case(tag_name);

self.skip_whitespace();

if close_tag_name == tag_name && self.peek() == '>' {
if closes_tag && self.peek() == '>' {
// Found the closing tag - emit any accumulated content
if consume_entities {
// For escapable raw text, Angular ALWAYS emits a text token, even if empty.
Expand All @@ -1815,11 +1831,12 @@ impl<'a> HtmlLexer<'a> {
}
}

// Emit the closing tag
// Emit the closing tag with the opening tag's parts, like
// upstream's `_endToken(openToken.parts)`.
self.advance(); // consume >
self.tokens.push(HtmlToken::with_prefix_name(
HtmlTokenType::TagClose,
"",
prefix,
tag_name,
saved_index,
self.index,
Expand Down Expand Up @@ -2010,7 +2027,9 @@ impl<'a> HtmlLexer<'a> {

self.skip_whitespace();

if close_name == expected_close && self.peek() == '>' {
// Upstream `_consumeRawTextWithTagClose` matches the close with
// `_attemptStrCaseInsensitive` for components too.
if close_name.eq_ignore_ascii_case(&expected_close) && self.peek() == '>' {
// Found the closing tag - emit content
let content = &self.input[content_start as usize..saved_index as usize];
let normalized = normalize_line_endings(content);
Expand Down Expand Up @@ -3076,8 +3095,11 @@ impl<'a> HtmlLexer<'a> {
// Handle `<` - only stop if it's a valid tag start
if ch == '<' {
let next = self.peek_at(1);
// Valid tag start: `/` (close tag), `!` (comment/doctype/cdata), or letter/underscore
if next == '/' || next == '!' || next.is_ascii_alphabetic() || next == '_' {
// Valid tag start: `/` (close tag), `!` (comment/doctype/cdata), or a letter.
// Upstream `_isTagStart` has no `_` case, so mid-text `<_C` stays
// text even in selectorless mode; a component only tokenizes at a
// token boundary (the dispatch check in `scan_next_token`).
if next == '/' || next == '!' || next.is_ascii_alphabetic() {
break;
}
// Otherwise, `<` is just text, continue
Expand Down
4 changes: 3 additions & 1 deletion crates/oxc_angular_compiler/src/parser/html/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,11 @@ impl<'a> HtmlParser<'a> {
fn auto_close_element_if_needed(&mut self, new_tag: &str) {
// Keep closing elements while the current one should be auto-closed
loop {
// The name as written: tag definitions are looked up case-insensitively, but
// the element is found again by its own name, which keeps its case (`<LI>`).
let current_tag = if let Some(&container_idx) = self.container_stack.last() {
if let ContainerIndex::Element(elem_idx) = container_idx {
self.elements[elem_idx].name.as_str().to_lowercase()
self.elements[elem_idx].name.to_string()
} else {
break;
}
Expand Down
17 changes: 15 additions & 2 deletions crates/oxc_angular_compiler/src/pipeline/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,8 +962,21 @@ fn ingest_node<'a>(job: &mut ComponentCompilationJob<'a>, view_xref: XrefId, nod
R3Node::IfBlockBranch(_) => {
// If branches are handled by the parent if block
}
R3Node::Component(_) | R3Node::Directive(_) | R3Node::HostElement(_) => {
// Components, directives, and host elements are resolved during binding/type checking
R3Node::Component(component) => {
// Only a selectorless parse produces these, and the pipeline cannot compile
// them. Angular's ingest skips them without a word; reporting the node keeps
// a template from compiling to less than it contains.
job.diagnostics.push(
OxcDiagnostic::error(format!(
"Selectorless component <{}> cannot be compiled: selectorless components \
are not supported.",
component.component_name
))
.with_label(component.source_span),
);
}
R3Node::Directive(_) | R3Node::HostElement(_) => {
// Directives and host elements are resolved during binding/type checking
}
}
}
Expand Down
Loading
Loading