@@ -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}
@@ -1215,7 +1228,12 @@ fn build_jit_decorator_text(
12151228///
12161229/// This runs as a post-pass after JIT text-edits, converting TypeScript → JavaScript.
12171230/// It handles abstract members, type annotations, parameter properties, etc.
1218- fn strip_typescript ( allocator : & Allocator , path : & str , code : & str ) -> String {
1231+ fn strip_typescript (
1232+ allocator : & Allocator ,
1233+ path : & str ,
1234+ code : & str ,
1235+ strip_uninitialized_fields : bool ,
1236+ ) -> String {
12191237 let source_type = SourceType :: from_path ( path) . unwrap_or_default ( ) ;
12201238 let parser_ret = Parser :: new ( allocator, code, source_type) . parse ( ) ;
12211239 if parser_ret. panicked {
@@ -1227,8 +1245,11 @@ fn strip_typescript(allocator: &Allocator, path: &str, code: &str) -> String {
12271245 let semantic_ret =
12281246 oxc_semantic:: SemanticBuilder :: new ( ) . with_excess_capacity ( 2.0 ) . build ( & program) ;
12291247
1230- let ts_options =
1231- oxc_transformer:: TypeScriptOptions { only_remove_type_imports : true , ..Default :: default ( ) } ;
1248+ let ts_options = oxc_transformer:: TypeScriptOptions {
1249+ only_remove_type_imports : true ,
1250+ remove_class_fields_without_initializer : strip_uninitialized_fields,
1251+ ..Default :: default ( )
1252+ } ;
12321253
12331254 let transform_options =
12341255 oxc_transformer:: TransformOptions { typescript : ts_options, ..Default :: default ( ) } ;
@@ -1560,7 +1581,8 @@ fn transform_angular_file_jit(
15601581 }
15611582
15621583 // 5. Strip TypeScript syntax from JIT output
1563- result. code = strip_typescript ( allocator, path, & result. code ) ;
1584+ result. code =
1585+ strip_typescript ( allocator, path, & result. code , options. strip_uninitialized_fields ) ;
15641586
15651587 result
15661588}
@@ -2300,6 +2322,50 @@ pub fn transform_angular_file(
23002322 }
23012323 }
23022324
2325+ // 4b. Strip uninitialized class fields (useDefineForClassFields: false behavior).
2326+ // This must process ALL classes, not just Angular-decorated ones, because
2327+ // legacy decorators (@Select, @Dispatch) set up prototype getters that get
2328+ // shadowed by _defineProperty(this, "field", void 0) when fields aren't stripped.
2329+ let mut uninitialized_field_spans: Vec < Span > = Vec :: new ( ) ;
2330+ if options. strip_uninitialized_fields {
2331+ for stmt in & parser_ret. program . body {
2332+ let class = match stmt {
2333+ Statement :: ClassDeclaration ( class) => Some ( class. as_ref ( ) ) ,
2334+ Statement :: ExportDefaultDeclaration ( export) => match & export. declaration {
2335+ ExportDefaultDeclarationKind :: ClassDeclaration ( class) => {
2336+ Some ( class. as_ref ( ) )
2337+ }
2338+ _ => None ,
2339+ } ,
2340+ Statement :: ExportNamedDeclaration ( export) => match & export. declaration {
2341+ Some ( Declaration :: ClassDeclaration ( class) ) => Some ( class. as_ref ( ) ) ,
2342+ _ => None ,
2343+ } ,
2344+ _ => None ,
2345+ } ;
2346+ let Some ( class) = class else { continue } ;
2347+ for element in & class. body . body {
2348+ if let ClassElement :: PropertyDefinition ( prop) = element {
2349+ // Skip static fields — they follow different rules
2350+ if prop. r#static {
2351+ continue ;
2352+ }
2353+ // Strip if: no initializer (value is None) OR has `declare` keyword
2354+ if prop. value . is_none ( ) || prop. declare {
2355+ let field_span = prop. span ;
2356+ // Remove any decorator spans that fall within this field span
2357+ // to avoid overlapping edits (which cause byte boundary panics).
2358+ decorator_spans_to_remove. retain ( |dec_span| {
2359+ !( dec_span. start >= field_span. start
2360+ && dec_span. end <= field_span. end )
2361+ } ) ;
2362+ uninitialized_field_spans. push ( field_span) ;
2363+ }
2364+ }
2365+ }
2366+ }
2367+ }
2368+
23032369 // 5. Generate output code using span-based edits from the original AST.
23042370 // All edits reference positions in the original source and are applied in one pass.
23052371
@@ -2364,6 +2430,21 @@ pub fn transform_angular_file(
23642430 edits. push ( Edit :: delete ( span. start , end as u32 ) ) ;
23652431 }
23662432
2433+ // Uninitialized field removal edits
2434+ for span in & uninitialized_field_spans {
2435+ let mut end = span. end as usize ;
2436+ let bytes = source. as_bytes ( ) ;
2437+ while end < bytes. len ( ) {
2438+ let c = bytes[ end] ;
2439+ if c == b' ' || c == b'\t' || c == b'\n' || c == b'\r' {
2440+ end += 1 ;
2441+ } else {
2442+ break ;
2443+ }
2444+ }
2445+ edits. push ( Edit :: delete ( span. start , end as u32 ) ) ;
2446+ }
2447+
23672448 if let Some ( edit) = ns_edit {
23682449 edits. push ( edit) ;
23692450 }
0 commit comments