Skip to content

Support useDefineForClassFields: false (Class Field Lowering) #73

Description

@tomer953

Summary

The OXC Angular compiler (@oxc-angular/vite) outputs native ES class fields without lowering them to constructor assignments. This causes runtime errors in Angular projects that use useDefineForClassFields: false in their tsconfig — which is the standard Angular configuration.

Runtime Errors

Error 1: Properties of undefined

TypeError: Cannot read properties of undefined (reading 'onClose')
    at <instance_members_initializer> ...
    at new CasesComponent

The <instance_members_initializer> in the V8 stack trace confirms that native class fields are being used at runtime, when they should have been lowered to constructor assignments.

Error 2: Private field SyntaxError

SyntaxError: Private field '#view' must be declared in an enclosing class

This occurs when private fields (#field) are completely removed from the class body during lowering — ES private fields require a class-level declaration for the private name slot.

Root Cause

The project's tsconfig.base.json has:

{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": false
  }
}

With useDefineForClassFields: false, TypeScript lowers class field initializers into the constructor body as assignments (legacy behavior). This is critical for Angular because:

  1. inject() in class fields: Angular's inject() function requires an active injection context. With useDefineForClassFields: false, inject() calls in class fields are lowered to constructor body assignments, where the injection context is guaranteed to be active.

  2. Constructor parameter properties: Angular components often use constructor DI (constructor(private router: Router)). With useDefineForClassFields: false, parameter properties are assigned before class field initializers in the constructor body. With native class fields, field initializers run before parameter property assignments.

  3. Inheritance: When a component extends a parent class, the parent's constructor sets properties via parameter properties. With useDefineForClassFields: false, the child's field initializers can safely reference these properties because they run after the parent constructor AND after the child's parameter property assignments.

Minimal Reproduction

Input TypeScript:

import { Component, inject } from '@angular/core';

class ParentClass {
  protected constructor(protected dep: SomeService) {}
}

class CifaPanelService {
  onClose: Observable<boolean>;
}

@Component({
  selector: 'app-cases',
  template: '<div>cases</div>',
  standalone: true
})
export class CasesComponent extends ParentClass {
  // Field using inject()
  private cifaPanelService = inject(CifaPanelService);

  // Field referencing the inject() field above — CRASHES with native class fields
  #casesDrawerCloseChangeEvent$ = this.cifaPanelService.onClose.pipe(delay(0));

  // Private field with signal
  #view = signal<string>('home');
  view = this.#view.asReadonly();

  constructor(protected dep: SomeService) {
    super(dep);
    console.log(this.#view());
  }
}

Current OXC Output (WRONG — keeps native class fields):

export class CasesComponent extends ParentClass {
  // These are NATIVE class fields — they run after super() but BEFORE constructor body
  cifaPanelService = inject(CifaPanelService);
  #casesDrawerCloseChangeEvent$ = this.cifaPanelService.onClose.pipe(delay(0));
  #view = signal('home');
  view = this.#view.asReadonly();

  constructor(dep) {
    super(dep);
    console.log(this.#view());
  }

  static ɵfac = function CasesComponent_Factory(__ngFactoryType__) { ... };
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ ... });
}

Expected OXC Output (with useDefineForClassFields: false):

export class CasesComponent extends ParentClass {
  #casesDrawerCloseChangeEvent$;  // ← Private field declaration KEPT
  #view;                          // ← Private field declaration KEPT

  constructor(dep) {
    super(dep);
    // Field initializers lowered to assignments (BEFORE existing constructor body)
    this.cifaPanelService = inject(CifaPanelService);
    this.#casesDrawerCloseChangeEvent$ = this.cifaPanelService.onClose.pipe(delay(0));
    this.#view = signal('home');
    this.view = this.#view.asReadonly();
    // Original constructor body
    console.log(this.#view());
  }

  static ɵfac = function CasesComponent_Factory(__ngFactoryType__) { ... };
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ ... });
}

Lowering Rules

Field type Class body Constructor body
Regular field (field = value) Remove declaration entirely Add this.field = value;
ES private field (#field = value) Keep declaration as #field; (no initializer) Add this.#field = value;
Static field (static field = value) Keep as-is (no lowering) Do NOT move
Field without initializer (field;) Keep as-is (no lowering) Do NOT move
declare field Keep as-is (no lowering) Do NOT move

Why private fields need special handling

ES private fields use a "private name" slot that must be established via a class-level declaration. Unlike regular properties, you cannot dynamically create # fields — the browser throws SyntaxError: Private field '#field' must be declared in an enclosing class.

Constructor body ordering

When lowering, the order in the constructor must be:

  1. super() call (if present)
  2. Lowered field initializer assignments (in declaration order)
  3. Original constructor body statements

This matches TypeScript's tsc behavior exactly.

Affected Patterns

Any Angular component/directive/service that:

  1. Uses inject() in a class field AND has another field that references the injected service
  2. References a constructor parameter property in a class field initializer
  3. Extends a parent class and references parent properties in class field initializers
  4. Uses ES private fields (#field) with initializers

Suggested Implementation

1. Add useDefineForClassFields option to TransformOptions

Rust (crates/oxc_angular_compiler/src/component/transform.rs):

pub struct TransformOptions {
    // ... existing fields ...

    /// Controls whether class fields use `[[Define]]` semantics (native ES class fields)
    /// or are lowered to constructor assignments.
    ///
    /// When `true` (default), class fields are kept as native ES class fields.
    /// When `false`, instance class field initializers are moved into the constructor body.
    pub use_define_for_class_fields: bool,
}

NAPI (napi/angular-compiler/src/lib.rs):

pub struct TransformOptions {
    // ... existing fields ...
    pub use_define_for_class_fields: Option<bool>,
}

2. Implement class field lowering pass

Create a new module crates/oxc_angular_compiler/src/component/class_field_lowering.rs with a lower_class_fields() function that:

  1. Parses the final transformed code
  2. For each class, identifies instance property definitions (non-static, with initializers)
  3. For regular fields: removes the declaration entirely from the class body
  4. For private fields: replaces the declaration with #field; (no initializer)
  5. Builds this.field = value; assignment statements
  6. Inserts assignments into the constructor body (after super() if present, before existing body)
  7. If no constructor exists, creates one (with super(...args) for subclasses)

Call this pass at the end of transform_angular_file() when use_define_for_class_fields is false.

3. Wire through Vite plugin

Read from tsconfig (napi/angular-compiler/vite-plugin/index.ts):

export interface PluginOptions {
  // ... existing options ...
  useDefineForClassFields?: boolean;
}

The plugin should:

  1. Accept an explicit useDefineForClassFields option
  2. If not provided, read it from the project's tsconfig.json (following the extends chain)
  3. Pass it to the Rust compiler via TransformOptions

4. Suggested tests

Unit tests (in class_field_lowering.rs):

  • test_lower_simple_class_fields — basic field lowering
  • test_lower_fields_with_super — lowering with super() call
  • test_static_fields_not_lowered — static fields preserved
  • test_no_constructor_creates_one — constructor created when missing
  • test_no_constructor_with_super_class — constructor with super(...args) for subclasses
  • test_private_fields_lowered — private field declaration kept, initializer moved
  • test_private_fields_declaration_kept_mixed — mixed private/regular fields
  • test_lowered_fields_before_existing_constructor_body — ordering verification
  • test_fields_without_initializer_not_lowered — declaration-only fields preserved

Integration tests (in integration_test.rs):

  • test_class_field_lowering_basic — full pipeline with @Component
  • test_class_field_lowering_disabled_by_default — no lowering when option is true
  • test_class_field_lowering_with_inheritance — extends + super() + private fields
  • test_class_field_lowering_directive — @Directive classes

Verification

const { transformAngularFileSync } = require('@oxc-angular/vite/api');

const code = `
import { Component, inject, signal } from '@angular/core';

class ParentClass {
  constructor(protected dep: any) {}
}

class MyService { onClose: any; }

@Component({ selector: 'app-test', template: '<div/>', standalone: true })
export class TestComponent extends ParentClass {
  private svc = inject(MyService);
  #event$ = this.svc.onClose.pipe();
  #view = signal('home');
  view = this.#view.asReadonly();
  constructor(protected dep: any) {
    super(dep);
    console.log(this.#view());
  }
}
`;

const result = transformAngularFileSync(code, 'test.ts',
  { sourcemap: false, jit: false, hmr: false, useDefineForClassFields: false },
  { templates: {}, styles: {} }
);
console.log(result.code);

// Expected output should show:
// 1. #event$; and #view; declarations kept in class body
// 2. All initializers moved to constructor body after super()
// 3. Original console.log() after the lowered assignments
// 4. Static ɵfac/ɵcmp fields untouched

Context

  • @oxc-angular/vite version: 0.0.8
  • Vite version: 8.0.0-beta.16 (uses Rolldown for bundling)
  • Angular version: 19/20
  • The OXC Angular compiler is used as a Vite plugin with order: 'pre'
  • Vite 8's built-in OXC transformer runs after the plugin and strips remaining TypeScript
  • Angular's standard tsconfig uses useDefineForClassFields: false

Activity

  1. tomer953 commented on Mar 5, 2026

    @tomer953
    Author

    @Brooooooklyn thanks for the fix attemp, but It did not solve the problem for me, here is what did solved:

    The Problem

    Angular projects use useDefineForClassFields: false in their tsconfig to get legacy [[Set]] semantics for class fields. This is critical because:

    1. Constructor parameter properties (e.g., protected userService: UserService) are assigned in the constructor body
    2. Class field initializers that reference inject() results or constructor parameter properties must run inside the constructor body in declaration order
    3. With native class fields (useDefineForClassFields: true), field initializers run after super() returns but before the constructor body — this breaks when fields reference constructor parameter properties or other injected fields

    Runtime Error

    TypeError: Cannot read properties of undefined (reading 'onClose')
        at <instance_members_initializer> (my.component.ts:42:56)
    

    The <instance_members_initializer> in the V8 stack trace confirms native class fields are being used at runtime.

    Affected Patterns

    // Pattern 1: inject() cross-reference (CRASHES)
    private panelService = inject(PanelService);
    #drawerCloseEvent$ = this.panelService.onClose.pipe(delay(0));
    
    // Pattern 2: Constructor parameter property reference (CRASHES)
    private isAdmin = this.userService.isAdmin();
    
    // Pattern 3: Arrow function fields referencing `this`
    initSession = (userId: string | null = null): void => {
      const name = this.getUserName(userId);
    };

    Why PR #82 (a6e98f0) Doesn't Work

    PR #82 added this to the plugin's config hook:

    ...(options.tsconfig && {
        build: {
            rolldownOptions: {
                tsconfig: options.tsconfig,
            },
        },
    }),

    This forwards the tsconfig to build.rolldownOptions.tsconfig, delegating class field lowering to Rolldown/Vite's built-in OXC transformer. However, this approach fundamentally cannot work due to the plugin execution order:

    The Transform Pipeline

    1. @oxc-angular/vite (order: 'pre')
       ├── Parses TypeScript
       ├── Compiles Angular templates/decorators (→ ɵɵdefineComponent, etc.)
       ├── Strips TypeScript types (access modifiers, type annotations)
       └── Outputs JAVASCRIPT with native class fields still intact
    
    2. vite:oxc (built-in, runs after 'pre' plugins)
       ├── Receives the already-transformed JavaScript code
       ├── File ID is still `.ts`, so the filter matches
       ├── Calls transformWithOxc() → Rolldown's transformSync()
       └── BUT: the code is already JavaScript — no TypeScript syntax remains
           → Class field lowering is NOT applied to JS content
    

    Three Specific Reasons

    1. build.rolldownOptions is for the build path (production bundling), not dev serve. During dev serve, Vite 8's vite:oxc plugin uses transformWithOxc() which resolves tsconfig per-file automatically.

    2. The OXC Angular plugin runs first (order: 'pre') and outputs JavaScript. By the time vite:oxc runs, all TypeScript syntax has been stripped.

    3. Class field lowering is a TypeScript transform — it's triggered by useDefineForClassFields: false in tsconfig, which only applies when processing TypeScript. Even if Rolldown's transformSync reads the tsconfig, it won't lower class fields in JavaScript content because that's a TS→JS lowering step.


    The Fix: Class Field Lowering in the OXC Angular Compiler

    Since the OXC Angular plugin is the one that strips TypeScript and outputs JavaScript, it must also handle class field lowering. The fix adds a new useDefineForClassFields option to TransformOptions and implements class field lowering as a post-processing step in transform_angular_file().

    Architecture

    transform_angular_file()
      ├── Parse TypeScript
      ├── Extract Angular metadata
      ├── Compile templates/decorators
      ├── Filter imports, remove decorators
      ├── Insert ɵcmp/ɵfac definitions
      └── NEW: If useDefineForClassFields == false
          └── lower_class_fields()  ← moves field initializers to constructor
    

    Lowering Rules

    Field type Class body Constructor body
    field = value Remove declaration Add this.field = value;
    #field = value Keep as #field; Add this.#field = value;
    static field = value Keep as-is Do NOT move
    field; (no init) Keep as-is Do NOT move
    field = () => { ... } Remove declaration Add this.field = () => { ... };

    Constructor Body Ordering

    1. super() call (if present)
    2. Lowered field initializer assignments (in declaration order)
    3. Original constructor body statements

    Implementation Details

    The lowering uses a sorted edits approach to avoid position corruption:

    1. Parse the final transformed code
    2. Collect all edits (field removals, initializer stripping, constructor insertions) with their original source positions
    3. Sort edits by position in descending order
    4. Apply edits from end to start, ensuring earlier edits don't shift positions of later edits

    This is critical because naive sequential modification (insert assignments, then remove fields) corrupts byte positions and produces invalid output like computele()); from computed<boolean>().

    Files Changed

    File Change
    crates/oxc_angular_compiler/src/component/class_field_lowering.rs New module implementing lower_class_fields()
    crates/oxc_angular_compiler/src/component/mod.rs Register the new module
    crates/oxc_angular_compiler/src/component/transform.rs Add use_define_for_class_fields to TransformOptions, call lowering
    napi/angular-compiler/src/lib.rs Add useDefineForClassFields to NAPI TransformOptions
    napi/angular-compiler/index.d.ts Add TypeScript type definition
    napi/angular-compiler/vite-plugin/index.ts Read useDefineForClassFields from tsconfig, pass to compiler

    Vite Plugin Changes

    The Vite plugin now:

    1. Reads useDefineForClassFields from the project's tsconfig (following the extends chain)
    2. Passes it to the Rust compiler via TransformOptions
    3. The Rust compiler performs class field lowering as part of transform_angular_file()
    // In the plugin's angular() function:
    const useDefineForClassFields = options.tsconfig
      ? readUseDefineForClassFields(resolve(workspaceRoot, options.tsconfig))
      : undefined;
    
    // In the transform hook:
    const transformOptions: TransformOptions = {
      sourcemap: pluginOptions.sourceMap,
      jit: pluginOptions.jit,
      hmr: pluginOptions.liveReload && watchMode,
      useDefineForClassFields,  // ← NEW
    };

    Testing

    Unit Tests (11 tests in class_field_lowering.rs)

    • test_lower_regular_fields — basic field lowering
    • test_lower_private_fields — ES private fields (#field)
    • test_skip_static_fields — static fields not lowered
    • test_super_call_ordering — assignments after super()
    • test_no_constructor_generates_one — auto-generate constructor
    • test_no_constructor_with_super_class — auto-generate with super(...args)
    • test_fields_without_initializer_not_lowered — field; kept as-is
    • test_angular_inject_pattern — full Angular inject cross-reference pattern
    • test_arrow_function_field — arrow function class fields
    • test_generic_call_in_field — computed<boolean>() preserved
    • test_optional_chaining_in_field — this.service?.getData() preserved

    Integration Tests (4 tests in integration_test.rs)

    • test_class_field_lowering_with_inject_pattern — full Angular component with inject, private fields, signals
    • test_class_field_lowering_not_applied_by_default — no lowering when option is not set
    • test_class_field_lowering_with_super_class — ordering with super() call
    • test_class_field_lowering_static_fields_untouched — static fields preserved

    Manual Testing Checklist

    1. Dev serve (npm run dev):

      • Components with inject() cross-references load without errors
      • Components extending parent classes work correctly
      • Arrow function class fields work correctly
      • computed<T>() and signal<T>() fields work correctly
      • Optional chaining in field initializers works
      • Static fields (ɵfac, ɵcmp) are NOT lowered
      • HMR still works for template/style changes
    2. Production build (npm run build):

      • Build completes without errors
      • Runtime behavior matches dev serve
    3. Edge cases:

      • Classes without constructors get auto-generated constructors
      • Classes extending parent classes get super(...args) forwarding
      • Multiple components in the same file all get lowered
      • Non-Angular classes in the same file also get lowered
      • Files without Angular decorators are not processed (early return)

    Verification Script

    const { transformAngularFileSync } = require('@oxc-angular/vite/api');
    
    const code = `
    import { Component, inject, signal } from '@angular/core';
    
    class MyService { onClose: any; }
    
    @Component({ selector: 'app-test', template: '<div/>', standalone: true })
    export class TestComponent {
      private svc = inject(MyService);
      #event$ = this.svc.onClose;
      #view = signal('home');
      view = this.#view.asReadonly();
      constructor() {
        console.log(this.#view());
      }
    }
    `;
    
    const result = transformAngularFileSync(code, 'test.ts',
      { sourcemap: false, jit: false, hmr: false, useDefineForClassFields: false },
      { templates: {}, styles: {} }
    );
    console.log(result.code);
    
    // Expected: field initializers moved to constructor body after super()
    // #event$; and #view; declarations kept in class body
    // Static ɵfac/ɵcmp fields untouched

    Why Private Fields Need Special Handling

    ES private fields use a "private name" slot that must be established via a class-level declaration. Unlike regular properties, you cannot dynamically create # fields:

    SyntaxError: Private field '#field' must be declared in an enclosing class
    

    So for private fields, we keep the declaration (#field;) but move only the initializer to the constructor (this.#field = value;).

  2. Brooooooklyn commented on Mar 6, 2026

    @Brooooooklyn
    Member

    You can either:

    1. Set the useDefineForClassFields: false to your tsconfig.json, Vite will resolve the nearest tsconfig.json that "include" the files that compiling.

    OR

    1. Add your tsconfig.app.json to the references field in your tsconfig.json

    OR

    1. Add this to your vite.config.ts:
    export default defineConfig({
      oxc: {
        typescript: {
          removeClassFieldsWithoutInitializer: true,
        },
        assumptions: {
          setPublicClassFields: true,
        }
      },
    })
  3. tomer953 commented on Mar 7, 2026

    @tomer953
    Author

    The author suggested three options. Here is the analysis of each, with a minimal reproduction and test results.

    Minimal Reproduction

    // vite.config.ts
    import { angular } from '@oxc-angular/vite';
    import { defineConfig } from 'vite';
    
    export default defineConfig({
      plugins: [
        angular({ tsconfig: './tsconfig.app.json' })
      ]
    });
    // tsconfig.json (root)
    {
      "compilerOptions": {
        "useDefineForClassFields": false,  // ← Angular standard
        "experimentalDecorators": true,
        "target": "ES2022"
      }
    }
    // tsconfig.app.json
    { "extends": "./tsconfig.json" }
    // app.component.ts — crashes at runtime
    import { Component, inject } from '@angular/core';
    import { delay } from 'rxjs';
    
    class PanelService {
      onClose = new Subject<void>();
    }
    
    @Component({ selector: 'app-root', template: '', standalone: true })
    export class AppComponent {
      // Pattern 1: public field cross-reference (crashes)
      private panelService = inject(PanelService);
      readonly events$ = this.panelService.onClose.pipe(delay(0));
    
      // Pattern 2: private # field cross-reference (crashes even harder)
      #events$ = this.panelService.onClose.pipe(delay(0));
    }

    Runtime error:

    TypeError: Cannot read properties of undefined (reading 'onClose')
        at <instance_members_initializer> (app.component.ts:12:34)
    

    The <instance_members_initializer> in the V8 stack trace confirms native class fields are being used — panelService is undefined when events$ initializes.


    Option 1: Set useDefineForClassFields: false in tsconfig.json

    "Vite will resolve the nearest tsconfig.json that 'include' the files that compiling."

    Result: ❌ Does NOT work

    useDefineForClassFields: false is already set in our root tsconfig.json (inherited by all project tsconfigs). Vite's built-in vite:oxc plugin does resolve the tsconfig per-file via TsconfigCache. However, the problem is the plugin execution order:

    1. @oxc-angular/vite runs first (order: 'pre') — it strips all TypeScript syntax and outputs JavaScript with native class fields intact
    2. vite:oxc runs after — it receives JavaScript, not TypeScript. Class field lowering is a TypeScript→JavaScript transform triggered by useDefineForClassFields: false. Since the code is already JavaScript, the tsconfig setting has no effect.

    The tsconfig is read correctly, but it's too late — the TypeScript has already been stripped.


    Option 2: Add tsconfig.app.json to the references field in tsconfig.json

    "Add your tsconfig.app.json to the references field in your tsconfig.json"

    Result: ❌ Does NOT work

    Our tsconfig.json already has references pointing to tsconfig.app.json. This is the standard Angular monorepo setup. The references field is for TypeScript project references (used by tsc --build), not for Vite's tsconfig resolution. Vite resolves tsconfig by walking up the directory tree from each source file, not via references. This option does not change the plugin execution order problem described in Option 1.


    Option 3: Add oxc config to vite.config.ts

    export default defineConfig({
      oxc: {
        typescript: { removeClassFieldsWithoutInitializer: true },
        assumptions: { setPublicClassFields: true }
      }
    })

    Result: ✅ Partially works — fixes public fields, does NOT fix private # fields

    We applied this option. Here is what happens:

    The oxc config is passed to Vite's built-in vite:oxc plugin, which calls rolldown/utils transformSync. Even though @oxc-angular/vite has already stripped TypeScript and output JavaScript, transformSync still applies the assumptions.setPublicClassFields transform to JavaScript content.

    The two options work together:

    • setPublicClassFields: true — moves public field initializers (field = value) to the constructor body as this.field = value
    • removeClassFieldsWithoutInitializer: true — removes the bare field; declaration that setPublicClassFields leaves behind (without this, you get a double-define via Object.defineProperty)

    Verified with rolldown/utils transformSync directly:

    // Input (JavaScript output from @oxc-angular/vite):
    class AppComponent {
      panelService = new PanelService();   // public field
      #events$ = this.panelService.onClose; // private # field
    }
    
    // Output with both options:
    class AppComponent {
      #events$ = this.panelService.onClose; // ← STILL a native field (not lowered)
      constructor() {
        this.panelService = new PanelService(); // ← lowered ✅
      }
    }

    Why # fields are not fixed: setPublicClassFields only applies to public fields. ES private fields (#field) require a class-level declaration slot — you cannot dynamically create them. So #events$ = this.panelService.onClose remains a native class field initializer that runs before the constructor body, and this.panelService is still undefined at that point.

    Workaround for # field cross-references: Declare the # field without an initializer and assign it in the constructor:

    // Before (crashes):
    private panelService = inject(PanelService);
    #events$ = this.panelService.onClose.pipe(delay(0));
    
    // After (works):
    private panelService = inject(PanelService);
    #events$!: Observable<void>;
    
    constructor() {
      this.#events$ = this.panelService.onClose.pipe(delay(0));
    }

    Summary Table

    Option Status Notes
    1. useDefineForClassFields: false in tsconfig ❌ Not effective Already set; vite:oxc receives JS not TS
    2. Add tsconfig.app.json to references ❌ Not effective Already done; doesn't change plugin order
    3. oxc: { assumptions: { setPublicClassFields }, typescript: { removeClassFieldsWithoutInitializer } } ⚠️ Partial Fixes public fields; # private fields still broken

    The complete fix requires the OXC Angular compiler to implement class field lowering natively in Rust (inside transform_angular_file()), so that both public and private fields are lowered before TypeScript is stripped.

  4. tomer953 commented on Mar 10, 2026

    @tomer953
    Author

    @Brooooooklyn could you please take a look at it?

    Also wanted to mention -
    Our app is very large and contains a lot of components, including legacy code and NgModules. Because of that, it’s really impressive to see this working so well. It significantly reduced our build times.

    The fact that this compiler can successfully serve and build such a large application is very promising.

    While testing it, I encountered several edge cases and complex scenarios and opened a few issues, as you’ve seen. This is one of the last runtime bugs we’re still facing.

    In my local fork I resolved the issue by implementing Class Field Lowering in Rust, and I can confirm that it fixes the runtime errors we observed.

    I also attached a simple reproduction so anyone (or even an AI) can reproduce and investigate the problem easily.

    Huge thanks for this project. I’m excited to see where it goes.

  5. Brooooooklyn commented on Mar 10, 2026

    @Brooooooklyn
    Member

    @tomer953 I'm discussing it with the Rolldown team

    BTW, can you share the performance changes in your project?

  6. tomer953 commented on Mar 10, 2026

    @tomer953
    Author

    Thanks 🙏

    I don't mind - but privately for now, if you want to DM on twitter

  7. Brooooooklyn commented on Mar 10, 2026

    @Brooooooklyn
    Member

    @tomer953 have you tried this?

    export default defineConfig({
      oxc: {
        target: 'es2015',
        typescript: { removeClassFieldsWithoutInitializer: true },
        assumptions: { setPublicClassFields: true }
      }
    })
  8. tomer953 commented on Mar 10, 2026

    @tomer953
    Author

    I mentioned that I tried all three options and mentioned what was wrong with each

    However I noticed you added

    target: 'es2015

    In the last comment, Im not sure I tried with it

    Do you want me to check?

  9. Brooooooklyn commented on Mar 10, 2026

    @Brooooooklyn
    Member

    Do you want me to check?

    Yes, adding target: 'es2015' should lower the syntax for private fields.

  10. tomer953 commented on Mar 10, 2026

    @tomer953
    Author

    seems it fixed the problem... good for me at this point

    but I'm not sure we want all the consumers to pass it right?
    I mean its a standard angular syntax with standard angular config

  11. Brooooooklyn commented on Mar 11, 2026

    @Brooooooklyn
    Member

    The oxc angular compiler only handles Angular-related compilation, such as HTML templates, CSS, and Angular built-in decorators.
    In simple terms, after oxc-angular-compiler compiles, it is still TypeScript, and the step of compiling from TypeScript to js is completed by Vite/Rolldown.

  12. added 2 commits that reference this issue on Apr 2, 2026
    0c3d78c
    2078247
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions