Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,16 @@ pub fn transform_angular_file(
if let Some(id) = &class.id {
let name = id.name.to_string();
if class_definitions.contains_key(&name) {
class_positions.push((name, stmt_start, class.body.span.end));
// Account for non-Angular decorators that precede the class.
// Decorators like @Log(...) appear before `export class` in source,
// so we must insert decls_before_class before those decorators.
let effective_start = class
.decorators
.iter()
.map(|d| d.span.start)
.min()
.map_or(stmt_start, |dec_start| dec_start.min(stmt_start));
class_positions.push((name, effective_start, class.body.span.end));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Custom (non-Angular) class decorators.
*
* Tests that non-Angular decorators are preserved in the Angular compiler output
* without breaking the generated code. The Angular compiler strips @Component
* but must leave custom decorators intact for downstream TS-to-JS tools
* (e.g., Rolldown) to lower.
*/
import type { Fixture } from '../types.js'

export const fixtures: Fixture[] = [
{
name: 'single-custom-decorator',
category: 'edge-cases',
description: 'Component with a single custom class decorator',
className: 'MyComponent',
type: 'full-transform',
sourceCode: `
import { Component } from '@angular/core';

function Log(message: string) {
return function <T extends new (...args: any[]) => any>(target: T): T {
console.log(message);
return target;
};
}

@Log('MyComponent loaded')
@Component({
selector: 'app-my',
template: '<span>hello</span>',
})
export class MyComponent {}
`,
expectedFeatures: ['ɵɵdefineComponent', 'ɵfac'],
},
{
name: 'multiple-custom-decorators',
category: 'edge-cases',
description: 'Component with multiple custom class decorators',
className: 'MultiDecoratorComponent',
type: 'full-transform',
sourceCode: `
import { Component } from '@angular/core';

function Sealed(target: any) { Object.seal(target); return target; }
function Track(name: string) {
return function(target: any) { return target; };
}

@Sealed
@Track('multi')
@Component({
selector: 'app-multi',
template: '<div>multi</div>',
})
export class MultiDecoratorComponent {}
`,
expectedFeatures: ['ɵɵdefineComponent', 'ɵfac'],
},
]
3 changes: 3 additions & 0 deletions napi/playground/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Component, signal } from '@angular/core'
import { RouterOutlet } from '@angular/router'

import { Log } from './custom-decorator'

@Log('App component loaded')
@Component({
selector: 'app-root',
imports: [RouterOutlet],
Expand Down
6 changes: 6 additions & 0 deletions napi/playground/src/app/custom-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function Log(message: string) {
return function <T extends new (...args: any[]) => any>(target: T): T {
console.log(message)
return target
}
}
Loading