Skip to content

Commit 872f726

Browse files
Brooooooklynclaude
andauthored
fix: account for non-Angular decorators when inserting decls_before_class (#63)
* fix: account for non-Angular decorators when inserting decls_before_class When a class has non-Angular decorators (e.g., @log), the const declarations (_c0, etc.) were inserted between the decorator and the class statement, producing invalid syntax. Now checks decorator span positions to insert before any preceding decorators. Vite/Rolldown's built-in oxc_transformer handles the TS-to-JS conversion (type stripping, decorator lowering) downstream, so no additional transform step is needed in the Angular compiler. Also adds a custom decorator to the playground for testing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add custom decorator e2e fixtures Add edge-case fixtures for components with non-Angular class decorators (single and multiple). Verifies the Angular compiler preserves custom decorators without breaking generated code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 55ab818 commit 872f726

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,7 +1313,16 @@ pub fn transform_angular_file(
13131313
if let Some(id) = &class.id {
13141314
let name = id.name.to_string();
13151315
if class_definitions.contains_key(&name) {
1316-
class_positions.push((name, stmt_start, class.body.span.end));
1316+
// Account for non-Angular decorators that precede the class.
1317+
// Decorators like @Log(...) appear before `export class` in source,
1318+
// so we must insert decls_before_class before those decorators.
1319+
let effective_start = class
1320+
.decorators
1321+
.iter()
1322+
.map(|d| d.span.start)
1323+
.min()
1324+
.map_or(stmt_start, |dec_start| dec_start.min(stmt_start));
1325+
class_positions.push((name, effective_start, class.body.span.end));
13171326
}
13181327
}
13191328
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Custom (non-Angular) class decorators.
3+
*
4+
* Tests that non-Angular decorators are preserved in the Angular compiler output
5+
* without breaking the generated code. The Angular compiler strips @Component
6+
* but must leave custom decorators intact for downstream TS-to-JS tools
7+
* (e.g., Rolldown) to lower.
8+
*/
9+
import type { Fixture } from '../types.js'
10+
11+
export const fixtures: Fixture[] = [
12+
{
13+
name: 'single-custom-decorator',
14+
category: 'edge-cases',
15+
description: 'Component with a single custom class decorator',
16+
className: 'MyComponent',
17+
type: 'full-transform',
18+
sourceCode: `
19+
import { Component } from '@angular/core';
20+
21+
function Log(message: string) {
22+
return function <T extends new (...args: any[]) => any>(target: T): T {
23+
console.log(message);
24+
return target;
25+
};
26+
}
27+
28+
@Log('MyComponent loaded')
29+
@Component({
30+
selector: 'app-my',
31+
template: '<span>hello</span>',
32+
})
33+
export class MyComponent {}
34+
`,
35+
expectedFeatures: ['ɵɵdefineComponent', 'ɵfac'],
36+
},
37+
{
38+
name: 'multiple-custom-decorators',
39+
category: 'edge-cases',
40+
description: 'Component with multiple custom class decorators',
41+
className: 'MultiDecoratorComponent',
42+
type: 'full-transform',
43+
sourceCode: `
44+
import { Component } from '@angular/core';
45+
46+
function Sealed(target: any) { Object.seal(target); return target; }
47+
function Track(name: string) {
48+
return function(target: any) { return target; };
49+
}
50+
51+
@Sealed
52+
@Track('multi')
53+
@Component({
54+
selector: 'app-multi',
55+
template: '<div>multi</div>',
56+
})
57+
export class MultiDecoratorComponent {}
58+
`,
59+
expectedFeatures: ['ɵɵdefineComponent', 'ɵfac'],
60+
},
61+
]

napi/playground/src/app/app.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Component, signal } from '@angular/core'
22
import { RouterOutlet } from '@angular/router'
33

4+
import { Log } from './custom-decorator'
5+
6+
@Log('App component loaded')
47
@Component({
58
selector: 'app-root',
69
imports: [RouterOutlet],
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function Log(message: string) {
2+
return function <T extends new (...args: any[]) => any>(target: T): T {
3+
console.log(message)
4+
return target
5+
}
6+
}

0 commit comments

Comments
 (0)