@@ -9,8 +9,9 @@ use std::path::Path;
99
1010use oxc_allocator:: { Allocator , Vec as OxcVec } ;
1111use oxc_ast:: ast:: {
12- Argument , ArrayExpressionElement , Declaration , ExportDefaultDeclarationKind , Expression ,
13- ImportDeclarationSpecifier , ImportOrExportKind , ObjectPropertyKind , PropertyKey , Statement ,
12+ Argument , ArrayExpressionElement , ClassElement , Declaration , ExportDefaultDeclarationKind ,
13+ Expression , ImportDeclarationSpecifier , ImportOrExportKind , ObjectPropertyKind , PropertyKey ,
14+ Statement ,
1415} ;
1516use oxc_diagnostics:: OxcDiagnostic ;
1617use oxc_parser:: Parser ;
@@ -182,6 +183,17 @@ pub struct TransformOptions {
182183 /// This runs after Angular style encapsulation, so it applies to the same
183184 /// final CSS strings that are embedded in component definitions.
184185 pub minify_component_styles : bool ,
186+
187+ /// Strip uninitialized class fields (matching `useDefineForClassFields: false` behavior).
188+ ///
189+ /// When true, class fields without an explicit initializer (`= ...`) are removed
190+ /// from the output. This matches TypeScript's behavior when `useDefineForClassFields`
191+ /// is `false` (Angular's default), where such fields are type-only declarations.
192+ ///
193+ /// Static fields and fields with initializers are always preserved.
194+ ///
195+ /// Default: true (Angular projects always use `useDefineForClassFields: false`)
196+ pub strip_uninitialized_fields : bool ,
185197}
186198
187199/// Input for host metadata when passed via TransformOptions.
@@ -232,6 +244,7 @@ impl Default for TransformOptions {
232244 // Class metadata for TestBed support (disabled by default)
233245 emit_class_metadata : false ,
234246 minify_component_styles : false ,
247+ strip_uninitialized_fields : true ,
235248 }
236249 }
237250}
@@ -1146,7 +1159,12 @@ fn build_jit_decorator_text(
11461159///
11471160/// This runs as a post-pass after JIT text-edits, converting TypeScript → JavaScript.
11481161/// It handles abstract members, type annotations, parameter properties, etc.
1149- fn strip_typescript ( allocator : & Allocator , path : & str , code : & str ) -> String {
1162+ fn strip_typescript (
1163+ allocator : & Allocator ,
1164+ path : & str ,
1165+ code : & str ,
1166+ strip_uninitialized_fields : bool ,
1167+ ) -> String {
11501168 let source_type = SourceType :: from_path ( path) . unwrap_or_default ( ) ;
11511169 let parser_ret = Parser :: new ( allocator, code, source_type) . parse ( ) ;
11521170 if parser_ret. panicked {
@@ -1158,8 +1176,11 @@ fn strip_typescript(allocator: &Allocator, path: &str, code: &str) -> String {
11581176 let semantic_ret =
11591177 oxc_semantic:: SemanticBuilder :: new ( ) . with_excess_capacity ( 2.0 ) . build ( & program) ;
11601178
1161- let ts_options =
1162- oxc_transformer:: TypeScriptOptions { only_remove_type_imports : true , ..Default :: default ( ) } ;
1179+ let ts_options = oxc_transformer:: TypeScriptOptions {
1180+ only_remove_type_imports : true ,
1181+ remove_class_fields_without_initializer : strip_uninitialized_fields,
1182+ ..Default :: default ( )
1183+ } ;
11631184
11641185 let transform_options =
11651186 oxc_transformer:: TransformOptions { typescript : ts_options, ..Default :: default ( ) } ;
@@ -1442,7 +1463,8 @@ fn transform_angular_file_jit(
14421463 }
14431464
14441465 // 5. Strip TypeScript syntax from JIT output
1445- result. code = strip_typescript ( allocator, path, & result. code ) ;
1466+ result. code =
1467+ strip_typescript ( allocator, path, & result. code , options. strip_uninitialized_fields ) ;
14461468
14471469 result
14481470}
@@ -2170,6 +2192,50 @@ pub fn transform_angular_file(
21702192 }
21712193 }
21722194
2195+ // 4b. Strip uninitialized class fields (useDefineForClassFields: false behavior).
2196+ // This must process ALL classes, not just Angular-decorated ones, because
2197+ // legacy decorators (@Select, @Dispatch) set up prototype getters that get
2198+ // shadowed by _defineProperty(this, "field", void 0) when fields aren't stripped.
2199+ let mut uninitialized_field_spans: Vec < Span > = Vec :: new ( ) ;
2200+ if options. strip_uninitialized_fields {
2201+ for stmt in & parser_ret. program . body {
2202+ let class = match stmt {
2203+ Statement :: ClassDeclaration ( class) => Some ( class. as_ref ( ) ) ,
2204+ Statement :: ExportDefaultDeclaration ( export) => match & export. declaration {
2205+ ExportDefaultDeclarationKind :: ClassDeclaration ( class) => {
2206+ Some ( class. as_ref ( ) )
2207+ }
2208+ _ => None ,
2209+ } ,
2210+ Statement :: ExportNamedDeclaration ( export) => match & export. declaration {
2211+ Some ( Declaration :: ClassDeclaration ( class) ) => Some ( class. as_ref ( ) ) ,
2212+ _ => None ,
2213+ } ,
2214+ _ => None ,
2215+ } ;
2216+ let Some ( class) = class else { continue } ;
2217+ for element in & class. body . body {
2218+ if let ClassElement :: PropertyDefinition ( prop) = element {
2219+ // Skip static fields — they follow different rules
2220+ if prop. r#static {
2221+ continue ;
2222+ }
2223+ // Strip if: no initializer (value is None) OR has `declare` keyword
2224+ if prop. value . is_none ( ) || prop. declare {
2225+ let field_span = prop. span ;
2226+ // Remove any decorator spans that fall within this field span
2227+ // to avoid overlapping edits (which cause byte boundary panics).
2228+ decorator_spans_to_remove. retain ( |dec_span| {
2229+ !( dec_span. start >= field_span. start
2230+ && dec_span. end <= field_span. end )
2231+ } ) ;
2232+ uninitialized_field_spans. push ( field_span) ;
2233+ }
2234+ }
2235+ }
2236+ }
2237+ }
2238+
21732239 // 5. Generate output code using span-based edits from the original AST.
21742240 // All edits reference positions in the original source and are applied in one pass.
21752241
@@ -2234,6 +2300,21 @@ pub fn transform_angular_file(
22342300 edits. push ( Edit :: delete ( span. start , end as u32 ) ) ;
22352301 }
22362302
2303+ // Uninitialized field removal edits
2304+ for span in & uninitialized_field_spans {
2305+ let mut end = span. end as usize ;
2306+ let bytes = source. as_bytes ( ) ;
2307+ while end < bytes. len ( ) {
2308+ let c = bytes[ end] ;
2309+ if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' {
2310+ end += 1 ;
2311+ } else {
2312+ break ;
2313+ }
2314+ }
2315+ edits. push ( Edit :: delete ( span. start , end as u32 ) ) ;
2316+ }
2317+
22372318 if let Some ( edit) = ns_edit {
22382319 edits. push ( edit) ;
22392320 }
0 commit comments