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
- Angular 20 uses modern decorators (
legacyDecorator: false)
- Signal inputs rely on modern decorator metadata to register inputs properly
- Hardcoded
legacyDecorator: true causes SWC to emit incompatible code
- 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:
-
Making legacyDecorator configurable via options:
swcAngularJestTransformer({
legacyDecorator: false, // Should be configurable
useDefineForClassFields: false
})
-
Defaulting to legacyDecorator: false for Angular 19+ projects (breaking change with major version bump)
-
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
Incompatibility with Angular 20 Signal Inputs:
legacyDecorator: trueHardcodedSummary
@jscutlery/swc-angularis incompatible with Angular 20 applications that use the moderninput()andinput.required()signal-based input functions. The library hardcodeslegacyDecorator: truein the SWC preset, which causesNG0950andNG0303errors during component testing.Environment
Issue Description
Angular 20 uses modern ECMAScript decorators by default and introduces signal-based component inputs using the
input()andinput.required()functions instead of the legacy@Input()decorator.The
swcAngularPreset()function in@jscutlery/swc-angularhardcodes the following transformation: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
This occurs when a component uses
input.required<T>()and the test callsfixture.componentRef.setInput()beforedetectChanges(). WithlegacyDecorator: true, Angular cannot properly initialize the signal inputs.Error 2: NG0303 - Unknown Property Error
Angular doesn't recognize properties defined with
input()orinput.required()as valid inputs when compiled with legacy decorators.Reproduction
Component Example (Angular 20 with Signal Inputs)
Test Example
Jest Configuration
Test Results
All failing tests use components with
input.required()orinput()signal-based inputs.Root Cause Analysis
Code Location
File:
packages/swc-angular/src/lib/jest-preset/index.ts(orindex.cjs.jsin published package)Why This Fails
legacyDecorator: false)legacyDecorator: truecauses SWC to emit incompatible codeswcAngularJestTransformer()optionsAttempted Workaround
❌ Workaround: Direct @swc/jest with .swcrc
Created custom
.swcrcwithlegacyDecorator: 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-angularshould support Angular 20's modern decorators and signal-based inputs by:Making
legacyDecoratorconfigurable via options:Defaulting to
legacyDecorator: falsefor Angular 19+ projects (breaking change with major version bump)Updating
@jscutlery/swc-angular-pluginto properly handle modern decorator metadata for signal inputsProposed Solution
Option 1: Add Configuration Parameter (Backward Compatible)
Option 2: Automatic Detection (Advanced)
Detect Angular version from
package.jsonand automatically set:legacyDecorator: truelegacyDecorator: falseImpact
This issue affects all Angular 20 applications using:
input(),input.required())model(),model.required())Given Angular's push toward signals in Angular 20+, this issue will impact an increasing number of projects.
Additional Context
Related Issues
References