Skip to content

Incompatibility with Angular 20 Signal Inputs: legacyDecorator: true hardcoded #976

@joseluis-balsera

Description

@joseluis-balsera

Incompatibility with Angular 20 Signal Inputs: legacyDecorator: true Hardcoded

Summary

@jscutlery/swc-angular is incompatible with Angular 20 applications that use the modern input() and input.required() signal-based input functions. The library hardcodes legacyDecorator: true in the SWC preset, which causes NG0950 and NG0303 errors during component testing.

Environment

  • @jscutlery/swc-angular: 0.22.0
  • @jscutlery/swc-angular-plugin: 0.22.0
  • @swc/core: 1.15.13
  • @swc/jest: 0.2.39
  • @angular/core: 20.3.16
  • jest-preset-angular: Latest
  • TypeScript: Latest
  • Node.js: Latest LTS

Issue Description

Angular 20 uses modern ECMAScript decorators by default and introduces signal-based component inputs using the input() and input.required() functions instead of the legacy @Input() decorator.

The swcAngularPreset() function in @jscutlery/swc-angular hardcodes the following transformation:

// packages/swc-angular/index.cjs.js (line 38)
transform: {
    legacyDecorator: true,  // ⚠️ HARDCODED
    decoratorMetadata: true,
    useDefineForClassFields: options.useDefineForClassFields
}

This setting is incompatible with Angular 20's modern decorator implementation and signal inputs, causing tests to fail with:

Error 1: NG0950 - Input Required Error

NG0950: Input is required but no value is available yet.

This occurs when a component uses input.required<T>() and the test calls fixture.componentRef.setInput() before detectChanges(). With legacyDecorator: true, Angular cannot properly initialize the signal inputs.

Error 2: NG0303 - Unknown Property Error

NG0303: Can't set value of the 'propertyName' input on the 'ComponentName' component. 
Make sure that the 'propertyName' property is declared as an input using the input() 
or model() function or the @Input() decorator.

Angular doesn't recognize properties defined with input() or input.required() as valid inputs when compiled with legacy decorators.

Reproduction

Component Example (Angular 20 with Signal Inputs)

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

@Component({
  selector: 'app-example',
  template: '<div>{{ data() }}</div>'
})
export class ExampleComponent {
  // Modern signal-based input (Angular 20)
  data = input.required<string>();
  
  // Computed signal derived from input
  processedData = computed(() => this.data().toUpperCase());
}

Test Example

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ExampleComponent } from './example.component';

describe('ExampleComponent', () => {
  let component: ExampleComponent;
  let fixture: ComponentFixture<ExampleComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ExampleComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(ExampleComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    // Set input before detectChanges() - standard Angular testing pattern
    fixture.componentRef.setInput('data', 'test');
    fixture.detectChanges();
    
    expect(component).toBeTruthy();
    // ❌ FAILS with NG0950 when using @jscutlery/swc-angular
    // ✅ PASSES with ts-jest
  });
});

Jest Configuration

// jest.config.js
const { swcAngularJestTransformer } = require('@jscutlery/swc-angular');

module.exports = {
  preset: 'jest-preset-angular',
  setupFilesAfterEnv: ['<rootDir>/jest-setup.ts'],
  transform: {
    '^.+\\.(ts|mjs|js)$': swcAngularJestTransformer({
      useDefineForClassFields: false
    }),
    '^.+\\.(html)$': ['jest-preset-angular', {
      tsconfig: '<rootDir>/tsconfig.spec.json',
    }],
  }
};

Test Results

  • With ts-jest: ✅ All tests pass
  • With @jscutlery/swc-angular: ❌ Tests fail with NG0950/NG0303 errors

All failing tests use components with input.required() or input() signal-based inputs.

Root Cause Analysis

Code Location

File: packages/swc-angular/src/lib/jest-preset/index.ts (or index.cjs.js in published package)

function swcAngularPreset(options = {}) {
    return {
        jsc: {
            parser: {
                syntax: 'typescript',
                decorators: true,
                dynamicImport: true
            },
            transform: {
                legacyDecorator: true,  // ❌ Problem: hardcoded, not configurable
                decoratorMetadata: true,
                useDefineForClassFields: options.useDefineForClassFields
            },
            // ...
        }
    };
}

Why This Fails

  1. Angular 20 uses modern decorators (legacyDecorator: false)
  2. Signal inputs rely on modern decorator metadata to register inputs properly
  3. Hardcoded legacyDecorator: true causes SWC to emit incompatible code
  4. No option to override this setting via swcAngularJestTransformer() options

Attempted Workaround

❌ Workaround: Direct @swc/jest with .swcrc

Created custom .swcrc with legacyDecorator: false:

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true,
      "dynamicImport": true
    },
    "transform": {
      "legacyDecorator": false,
      "decoratorMetadata": true,
      "useDefineForClassFields": false
    },
    "experimental": {
      "plugins": [
        ["@jscutlery/swc-angular-plugin", {}]
      ]
    },
    "target": "es2022",
    "keepClassNames": true
  },
  "module": {
    "type": "commonjs"
  }
}

Result: Templates/styles are inlined correctly, but same NG0950/NG0303 errors persist. This suggests the SWC plugin itself may need updates for Angular 20.

Expected Behavior

@jscutlery/swc-angular should support Angular 20's modern decorators and signal-based inputs by:

  1. Making legacyDecorator configurable via options:

    swcAngularJestTransformer({
      legacyDecorator: false,  // Should be configurable
      useDefineForClassFields: false
    })
  2. Defaulting to legacyDecorator: false for Angular 19+ projects (breaking change with major version bump)

  3. Updating @jscutlery/swc-angular-plugin to properly handle modern decorator metadata for signal inputs

Proposed Solution

Option 1: Add Configuration Parameter (Backward Compatible)

export interface SwcAngularOptions {
  useDefineForClassFields?: boolean;
  legacyDecorator?: boolean;  // NEW: Allow users to override
  importStyles?: boolean;
  styleInlineSuffix?: boolean;
  templateRawSuffix?: boolean;
}

function swcAngularPreset(options: SwcAngularOptions = {}) {
    return {
        jsc: {
            // ...
            transform: {
                legacyDecorator: options.legacyDecorator ?? true,  // Default to true for BC
                decoratorMetadata: true,
                useDefineForClassFields: options.useDefineForClassFields
            },
            // ...
        }
    };
}

Option 2: Automatic Detection (Advanced)

Detect Angular version from package.json and automatically set:

  • Angular < 19: legacyDecorator: true
  • Angular >= 19: legacyDecorator: false

Impact

This issue affects all Angular 20 applications using:

  • Modern signal-based inputs (input(), input.required())
  • Model signals (model(), model.required())
  • Modern ECMAScript decorators

Given Angular's push toward signals in Angular 20+, this issue will impact an increasing number of projects.

Additional Context

Related Issues

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions