@@ -14,6 +14,8 @@ use oxc_ast::ast::{
1414use oxc_diagnostics:: OxcDiagnostic ;
1515use oxc_span:: { GetSpan , Span } ;
1616use oxc_str:: Ident ;
17+ use oxc_syntax:: number:: ToJsString ;
18+ use oxc_syntax:: operator:: UnaryOperator ;
1719
1820use super :: evaluator:: { Evaluator , FileScope , Prop , RefKind , Value , transform_error} ;
1921use super :: metadata:: {
@@ -490,6 +492,8 @@ fn has_ng_on_changes_method(class: &Class<'_>) -> bool {
490492#[ derive( Default ) ]
491493pub struct StringConsts < ' a > {
492494 strings : HashMap < & ' a str , Ident < ' a > > ,
495+ /// `const NAME = 123`: numbers a template literal's `${...}` can interpolate.
496+ numbers : HashMap < & ' a str , f64 > ,
493497 program : Option < & ' a Program < ' a > > ,
494498 /// Built on first use: most files have no decorator metadata to evaluate.
495499 scope : std:: cell:: OnceCell < FileScope < ' a > > ,
@@ -535,6 +539,7 @@ impl<'a> StringConsts<'a> {
535539 pub ( crate ) fn declarations_of ( program : & ' a Program < ' a > ) -> Self {
536540 Self {
537541 strings : HashMap :: default ( ) ,
542+ numbers : HashMap :: default ( ) ,
538543 program : Some ( program) ,
539544 scope : std:: cell:: OnceCell :: new ( ) ,
540545 #[ cfg( feature = "cross_file_elision" ) ]
@@ -594,6 +599,10 @@ pub fn collect_string_consts<'a>(
594599
595600 let mut map = StringConsts {
596601 strings : HashMap :: default ( ) ,
602+ numbers : pending
603+ . iter ( )
604+ . filter_map ( |( name, init) | Some ( ( * name, numeric_literal_value ( init) ?) ) )
605+ . collect ( ) ,
597606 program : Some ( program) ,
598607 scope : std:: cell:: OnceCell :: new ( ) ,
599608 #[ cfg( feature = "cross_file_elision" ) ]
@@ -632,9 +641,23 @@ fn literal_string_from_expression<'a>(expr: &Expression<'a>) -> Option<Ident<'a>
632641 }
633642}
634643
644+ /// The value of a number literal (`150`, `-1.5`).
645+ fn numeric_literal_value ( expr : & Expression < ' _ > ) -> Option < f64 > {
646+ match expr {
647+ Expression :: NumericLiteral ( lit) => Some ( lit. value ) ,
648+ Expression :: UnaryExpression ( unary) if unary. operator == UnaryOperator :: UnaryNegation => {
649+ match & unary. argument {
650+ Expression :: NumericLiteral ( lit) => Some ( -lit. value ) ,
651+ _ => None ,
652+ }
653+ }
654+ _ => None ,
655+ }
656+ }
657+
635658/// Fold a template literal whose `${...}` interpolations reference known
636- /// string consts. Returns `None` if any interpolation can't be statically
637- /// resolved to a string — matching Angular's all-or-nothing partial evaluator
659+ /// string consts, number consts or number literals . Returns `None` if any
660+ /// interpolation can't be statically resolved to a string — matching Angular's all-or-nothing partial evaluator
638661/// for static metadata fields.
639662pub ( crate ) fn resolve_template_literal < ' a > (
640663 allocator : & ' a Allocator ,
@@ -651,8 +674,17 @@ pub(crate) fn resolve_template_literal<'a>(
651674 let cooked = quasi. value . cooked ?. as_str ( ) ?;
652675 buf. push_str ( cooked) ;
653676 if let Some ( expr) = tpl. expressions . get ( i) {
654- let resolved = extract_string_value ( allocator, expr, consts) ?;
655- buf. push_str ( resolved. as_str ( ) ) ;
677+ if let Some ( resolved) = extract_string_value ( allocator, expr, consts) {
678+ buf. push_str ( resolved. as_str ( ) ) ;
679+ } else {
680+ // `${150}` or `${WIDTH}` with `const WIDTH = 150`: the number as
681+ // JavaScript converts it to a string
682+ let number = match expr {
683+ Expression :: Identifier ( id) => * consts. numbers . get ( id. name . as_str ( ) ) ?,
684+ _ => numeric_literal_value ( expr) ?,
685+ } ;
686+ buf. push_str ( & number. to_js_string ( ) ) ;
687+ }
656688 }
657689 }
658690 Some ( Ident :: from ( allocator. alloc_str ( & buf) ) )
0 commit comments