diff --git a/crates/oxc_angular_compiler/src/parser/html/lexer.rs b/crates/oxc_angular_compiler/src/parser/html/lexer.rs
index 2b03220dd..059d82b6f 100644
--- a/crates/oxc_angular_compiler/src/parser/html/lexer.rs
+++ b/crates/oxc_angular_compiler/src/parser/html/lexer.rs
@@ -1360,8 +1360,9 @@ impl<'a> HtmlLexer<'a> {
break;
}
- // EOF or tag start ends the value
- if ch == chars::EOF || ch == '<' {
+ // EOF ends the value. A `<` does not (`@let a = b < c;`): like Angular's
+ // `_consumeLetDeclarationValue`, only a semicolon outside quotes ends it.
+ if ch == chars::EOF {
break;
}
diff --git a/crates/oxc_angular_compiler/tests/html_lexer_test.rs b/crates/oxc_angular_compiler/tests/html_lexer_test.rs
index 8a1202c16..b950f3ce6 100644
--- a/crates/oxc_angular_compiler/tests/html_lexer_test.rs
+++ b/crates/oxc_angular_compiler/tests/html_lexer_test.rs
@@ -1675,6 +1675,36 @@ mod let_declarations {
);
}
+ #[test]
+ fn should_parse_let_declaration_with_less_than_in_its_value() {
+ // Angular's `_consumeLetDeclarationValue` ends the value only at a `;`
+ // outside quotes (or EOF): a `<` is a comparison, not a tag
+ let result = tokenize_and_humanize_parts("@let small = count < 10;");
+ assert_eq!(
+ result,
+ vec![
+ (HtmlTokenType::LetStart, vec!["small".to_string()]),
+ (HtmlTokenType::LetValue, vec!["count < 10".to_string()]),
+ (HtmlTokenType::LetEnd, vec![]),
+ (HtmlTokenType::Eof, vec![]),
+ ]
+ );
+ }
+
+ #[test]
+ fn should_parse_let_declaration_with_comparisons_followed_by_an_element() {
+ let result = tokenize_and_humanize_parts("@let both = a < b && b < c;");
+ assert_eq!(
+ result[..3],
+ [
+ (HtmlTokenType::LetStart, vec!["both".to_string()]),
+ (HtmlTokenType::LetValue, vec!["a < b && b < c".to_string()]),
+ (HtmlTokenType::LetEnd, vec![]),
+ ]
+ );
+ assert_eq!(result[3].0, HtmlTokenType::TagOpenStart);
+ }
+
#[test]
fn should_report_incomplete_let_without_equals_after_name() {
// No upstream spec case; Angular's _consumeLetDeclaration marks the
diff --git a/crates/oxc_angular_compiler/tests/let_value_less_than_test.rs b/crates/oxc_angular_compiler/tests/let_value_less_than_test.rs
new file mode 100644
index 000000000..ca1b8f49a
--- /dev/null
+++ b/crates/oxc_angular_compiler/tests/let_value_less_than_test.rs
@@ -0,0 +1,32 @@
+//! A `@let` value can contain `<` (`@let small = count < 10;`): as in Angular's
+//! lexer, only a `;` outside quotes ends it. `@angular/compiler-cli` 21.0.1
+//! compiles the template below to `const small_r1 = ctx.count < 10;` and
+//! `const both_r2 = ctx.a < ctx.b && ctx.b < ctx.c;`.
+
+use oxc_allocator::Allocator;
+use oxc_angular_compiler::{TransformOptions, transform_angular_file};
+
+#[test]
+fn let_value_with_less_than_compiles_as_a_comparison() {
+ let source = r"
+import { Component } from '@angular/core';
+@Component({
+ selector: 'b-cmp',
+ template: `@let small = count < 10; @let both = a < b && b < c;
+{{ small }} {{ both }}`,
+})
+export class BCmp { count = 1; a = 1; b = 2; c = 3; }
+";
+ let allocator = Allocator::default();
+ let result = transform_angular_file(
+ &allocator,
+ "test.ts",
+ source,
+ Some(&TransformOptions::default()),
+ None,
+ );
+ assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
+ let code: String = result.code.chars().filter(|c| !c.is_whitespace()).collect();
+ assert!(code.contains("constsmall_r1=(ctx.count<10);"), "Got:\n{}", result.code);
+ assert!(code.contains("constboth_r2=((ctx.a