Skip to content

fix(jit): @Inject / @Optional / @Attribute on a class member are dropped instead of lowered to propDecorators #522

Description

@Brooooooklyn

In JIT mode (jit: true), OXC drops @angular/core member decorators that are not field decorators. Examples are @Inject, @Optional, @Attribute, @Self, @Host, and @Component on a member. ngtsc's JIT transform (downlevel_decorators_transform) lowers every member decorator imported from @angular/core into static propDecorators. It does not check the decorator name.

The code says this on purpose (component/transform.rs, near ANGULAR_DECORATOR_NAMES):

// Angular non-field decorators (e.g. @Inject on a member) are silently dropped
// since they have no meaningful effect on members.

Angular's own JIT compiler ignores these entries. But code that reads propDecorators (for example ReflectionCapabilities.propMetadata, or custom tools) sees a different result. The output is not 1:1 with ngtsc.

Repro

// tokens.ts
import {InjectionToken} from '@angular/core';
export const TOKEN = new InjectionToken<unknown>('T');

// test.ts
import {Component, Inject, Optional, Attribute} from '@angular/core';
import {TOKEN} from './tokens';
@Component({selector: 'c', template: ''})
export class C {
  @Inject(TOKEN) @Optional() x: unknown;
  @Attribute('a') y: unknown;
}

OXC (transformAngularFileSync(src, file, { jit: true }), main d34d9b9)

import { Component, Inject, Optional, Attribute } from "@angular/core";
import { TOKEN } from "./tokens";
import { __decorate } from "tslib";
let C = class C {
	x;
	y;
};
C = __decorate([Component({
	selector: "c",
	template: ""
})], C);
export { C };

ngtsc 22.1.7 (constructorParametersDownlevelTransform, which @angular/build uses for JIT)

import { Component, Inject, Optional, Attribute } from '@angular/core';
import { TOKEN } from './tokens';
let C = class C {
    static { this.propDecorators = {
        x: [{ type: Inject, args: [TOKEN,] }, { type: Optional }],
        y: [{ type: Attribute, args: ['a',] }]
    }; }
};
C = __decorate([
    Component({ selector: 'c', template: '' })
], C);
export { C };

Expected

Lower every member decorator imported from @angular/core into propDecorators, like ngtsc's isAngularDecorator (decorator.import.from === "@angular/core"). Keep the decorator order. A mixed member (@Input() @Inject(T) x) must keep both entries. Today OXC keeps only Input.

Found during the review of #504. #504 already makes the member decorator check import-aware: a same-named decorator from another module stays in __decorate().

Activity

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

    bugSomething isn't working

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions