Skip to content

Commit 2d4c849

Browse files
Brooooooklynclaude
andcommitted
feat: implement JIT compilation output for transformAngularFile
When `jit: true` is passed, the compiler now produces Angular JIT-compatible output instead of AOT-compiled code. This matches the output format of Angular CLI's JitCompilation class: - Decorator downleveling via `__decorate` from tslib - templateUrl/styleUrl replaced with `angular:jit:template:file;` imports - Constructor params emitted as `static ctorParameters` for runtime DI - Class restructured to `let X = class X {}; X = __decorate([...], X);` - Templates are NOT compiled (runtime JIT compiler handles that) - Import elision disabled (ctor param types needed at runtime) Closes #97 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 03650a7 commit 2d4c849

9 files changed

+1113
-3
lines changed

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 586 additions & 3 deletions
Large diffs are not rendered by default.

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5857,3 +5857,391 @@ fn test_host_binding_pure_function_declarations_emitted() {
58575857
}
58585858
}
58595859
}
5860+
5861+
// ============================================================================
5862+
// Standalone Emission Tests (Issue #95)
5863+
// ============================================================================
5864+
5865+
/// Test that a standalone component does NOT emit `standalone:true` in ɵɵdefineComponent.
5866+
///
5867+
/// Angular's TS compiler (compiler.ts:96-98) only emits `standalone: false` when
5868+
/// isStandalone === false. When standalone is true, it's omitted because the Angular
5869+
/// runtime (definition.ts:637) defaults `standalone` to `true` via `?? true`.
5870+
///
5871+
/// OXC matches this behavior exactly.
5872+
#[test]
5873+
fn test_standalone_component_omits_standalone_field() {
5874+
let allocator = Allocator::default();
5875+
let source = r#"
5876+
import { Component } from '@angular/core';
5877+
5878+
@Component({
5879+
selector: 'app-test',
5880+
standalone: true,
5881+
template: '<div>test</div>'
5882+
})
5883+
export class TestComponent {}
5884+
"#;
5885+
5886+
let options = ComponentTransformOptions::default();
5887+
let result = transform_angular_file(&allocator, "test.component.ts", source, &options, None);
5888+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
5889+
5890+
let normalized = result.code.replace([' ', '\n', '\t'], "");
5891+
// Angular TS compiler omits standalone when true (runtime defaults to true via ?? true)
5892+
assert!(
5893+
!normalized.contains("standalone:true"),
5894+
"Standalone component should NOT emit `standalone:true` (runtime defaults to true). Output:\n{}",
5895+
result.code
5896+
);
5897+
}
5898+
5899+
/// Test that a non-standalone component emits `standalone:false` in ɵɵdefineComponent.
5900+
#[test]
5901+
fn test_non_standalone_component_emits_standalone_false() {
5902+
let allocator = Allocator::default();
5903+
let source = r#"
5904+
import { Component } from '@angular/core';
5905+
5906+
@Component({
5907+
selector: 'app-legacy',
5908+
standalone: false,
5909+
template: '<div>legacy</div>'
5910+
})
5911+
export class LegacyComponent {}
5912+
"#;
5913+
5914+
let options = ComponentTransformOptions::default();
5915+
let result = transform_angular_file(&allocator, "test.component.ts", source, &options, None);
5916+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
5917+
5918+
let normalized = result.code.replace([' ', '\n', '\t'], "");
5919+
assert!(
5920+
normalized.contains("standalone:false"),
5921+
"Non-standalone component MUST emit `standalone:false` in ɵɵdefineComponent. Output:\n{}",
5922+
result.code
5923+
);
5924+
}
5925+
5926+
/// Test that an implicit standalone component (Angular 19+ default) omits `standalone` field.
5927+
///
5928+
/// Angular 19+ defaults `standalone` to `true`. The Angular TS compiler omits the field
5929+
/// when true, and the runtime defaults it via `?? true`. OXC matches this behavior.
5930+
#[test]
5931+
fn test_implicit_standalone_with_imports_omits_standalone_field() {
5932+
let allocator = Allocator::default();
5933+
let source = r#"
5934+
import { Component } from '@angular/core';
5935+
import { NgIf } from '@angular/common';
5936+
5937+
@Component({
5938+
selector: 'app-implicit',
5939+
imports: [NgIf],
5940+
template: '<div *ngIf="true">implicit standalone</div>'
5941+
})
5942+
export class ImplicitStandaloneComponent {}
5943+
"#;
5944+
5945+
let options = ComponentTransformOptions::default();
5946+
let result = transform_angular_file(&allocator, "test.component.ts", source, &options, None);
5947+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
5948+
5949+
let normalized = result.code.replace([' ', '\n', '\t'], "");
5950+
// Angular TS compiler omits standalone when true (runtime defaults to true via ?? true)
5951+
assert!(
5952+
!normalized.contains("standalone:true"),
5953+
"Implicit standalone component should NOT emit `standalone:true` (runtime defaults to true). Output:\n{}",
5954+
result.code
5955+
);
5956+
}
5957+
5958+
// ============================================================================
5959+
// JIT Compilation Tests
5960+
// ============================================================================
5961+
5962+
#[test]
5963+
fn test_jit_component_with_inline_template() {
5964+
// When jit: true, the compiler should NOT compile templates.
5965+
// Instead, it should keep the decorator and downlevel it using __decorate.
5966+
let allocator = Allocator::default();
5967+
let source = r#"
5968+
import { Component } from '@angular/core';
5969+
5970+
@Component({
5971+
selector: 'app-root',
5972+
template: '<h1>Hello</h1>',
5973+
standalone: true,
5974+
})
5975+
export class AppComponent {}
5976+
"#;
5977+
5978+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
5979+
let result = transform_angular_file(&allocator, "app.component.ts", source, &options, None);
5980+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
5981+
5982+
// Should have __decorate import from tslib
5983+
assert!(
5984+
result.code.contains("import { __decorate } from \"tslib\""),
5985+
"JIT output should import __decorate from tslib. Got:\n{}",
5986+
result.code
5987+
);
5988+
5989+
// Should NOT have ɵcmp or ɵfac (AOT-style definitions)
5990+
assert!(
5991+
!result.code.contains("ɵcmp") && !result.code.contains("ɵfac"),
5992+
"JIT output should NOT contain AOT definitions (ɵcmp/ɵfac). Got:\n{}",
5993+
result.code
5994+
);
5995+
5996+
// Should have __decorate call with Component
5997+
assert!(
5998+
result.code.contains("__decorate("),
5999+
"JIT output should use __decorate. Got:\n{}",
6000+
result.code
6001+
);
6002+
6003+
// Should keep the template property as-is (inline template)
6004+
assert!(
6005+
result.code.contains("template:"),
6006+
"JIT output should preserve inline template. Got:\n{}",
6007+
result.code
6008+
);
6009+
6010+
insta::assert_snapshot!("jit_inline_template", result.code);
6011+
}
6012+
6013+
#[test]
6014+
fn test_jit_component_with_template_url() {
6015+
// When jit: true and templateUrl is used, it should be replaced with
6016+
// an import from angular:jit:template:file;./path
6017+
let allocator = Allocator::default();
6018+
let source = r#"
6019+
import { Component } from '@angular/core';
6020+
6021+
@Component({
6022+
selector: 'app-root',
6023+
templateUrl: './app.html',
6024+
standalone: true,
6025+
})
6026+
export class AppComponent {}
6027+
"#;
6028+
6029+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
6030+
let result = transform_angular_file(&allocator, "app.component.ts", source, &options, None);
6031+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
6032+
6033+
// Should have resource import for template
6034+
assert!(
6035+
result.code.contains("angular:jit:template:file;./app.html"),
6036+
"JIT output should import template via angular:jit:template:file. Got:\n{}",
6037+
result.code
6038+
);
6039+
6040+
// Should replace templateUrl with template referencing the import
6041+
assert!(
6042+
!result.code.contains("templateUrl"),
6043+
"JIT output should replace templateUrl with template. Got:\n{}",
6044+
result.code
6045+
);
6046+
6047+
insta::assert_snapshot!("jit_template_url", result.code);
6048+
}
6049+
6050+
#[test]
6051+
fn test_jit_component_with_style_url() {
6052+
// When jit: true and styleUrl/styleUrls is used, it should be replaced with
6053+
// imports from angular:jit:style:file;./path
6054+
let allocator = Allocator::default();
6055+
let source = r#"
6056+
import { Component } from '@angular/core';
6057+
6058+
@Component({
6059+
selector: 'app-root',
6060+
template: '<h1>Hello</h1>',
6061+
styleUrl: './app.css',
6062+
})
6063+
export class AppComponent {}
6064+
"#;
6065+
6066+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
6067+
let result = transform_angular_file(&allocator, "app.component.ts", source, &options, None);
6068+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
6069+
6070+
// Should have resource import for style
6071+
assert!(
6072+
result.code.contains("angular:jit:style:file;./app.css"),
6073+
"JIT output should import style via angular:jit:style:file. Got:\n{}",
6074+
result.code
6075+
);
6076+
6077+
insta::assert_snapshot!("jit_style_url", result.code);
6078+
}
6079+
6080+
#[test]
6081+
fn test_jit_component_with_constructor_deps() {
6082+
// JIT compilation should generate ctorParameters for constructor dependencies
6083+
let allocator = Allocator::default();
6084+
let source = r#"
6085+
import { Component } from '@angular/core';
6086+
import { TitleService } from './title.service';
6087+
6088+
@Component({
6089+
selector: 'app-root',
6090+
template: '<h1>Hello</h1>',
6091+
})
6092+
export class AppComponent {
6093+
constructor(private titleService: TitleService) {}
6094+
}
6095+
"#;
6096+
6097+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
6098+
let result = transform_angular_file(&allocator, "app.component.ts", source, &options, None);
6099+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
6100+
6101+
// Should have ctorParameters static property
6102+
assert!(
6103+
result.code.contains("ctorParameters"),
6104+
"JIT output should contain ctorParameters. Got:\n{}",
6105+
result.code
6106+
);
6107+
6108+
// Should reference TitleService type
6109+
assert!(
6110+
result.code.contains("TitleService"),
6111+
"JIT ctorParameters should reference dependency type. Got:\n{}",
6112+
result.code
6113+
);
6114+
6115+
insta::assert_snapshot!("jit_constructor_deps", result.code);
6116+
}
6117+
6118+
#[test]
6119+
fn test_jit_component_class_restructuring() {
6120+
// JIT should restructure: export class X {} → let X = class X {}; X = __decorate([...], X); export { X };
6121+
let allocator = Allocator::default();
6122+
let source = r#"
6123+
import { Component } from '@angular/core';
6124+
6125+
@Component({
6126+
selector: 'app-root',
6127+
template: '<h1>Hello</h1>',
6128+
})
6129+
export class AppComponent {
6130+
title = 'app';
6131+
}
6132+
"#;
6133+
6134+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
6135+
let result = transform_angular_file(&allocator, "app.component.ts", source, &options, None);
6136+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
6137+
6138+
// Should have let declaration
6139+
assert!(
6140+
result.code.contains("let AppComponent = class AppComponent"),
6141+
"JIT output should use 'let X = class X' pattern. Got:\n{}",
6142+
result.code
6143+
);
6144+
6145+
// Should have export statement
6146+
assert!(
6147+
result.code.contains("export { AppComponent }"),
6148+
"JIT output should have named export. Got:\n{}",
6149+
result.code
6150+
);
6151+
6152+
insta::assert_snapshot!("jit_class_restructuring", result.code);
6153+
}
6154+
6155+
#[test]
6156+
fn test_jit_directive() {
6157+
// @Directive should also be JIT-transformed with __decorate
6158+
let allocator = Allocator::default();
6159+
let source = r#"
6160+
import { Directive, Input } from '@angular/core';
6161+
6162+
@Directive({
6163+
selector: '[appHighlight]',
6164+
standalone: true,
6165+
})
6166+
export class HighlightDirective {
6167+
@Input() color: string = 'yellow';
6168+
}
6169+
"#;
6170+
6171+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
6172+
let result =
6173+
transform_angular_file(&allocator, "highlight.directive.ts", source, &options, None);
6174+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
6175+
6176+
// Should have __decorate with Directive
6177+
assert!(
6178+
result.code.contains("__decorate("),
6179+
"JIT directive output should use __decorate. Got:\n{}",
6180+
result.code
6181+
);
6182+
6183+
// Should NOT have ɵdir or ɵfac
6184+
assert!(
6185+
!result.code.contains("ɵdir") && !result.code.contains("ɵfac"),
6186+
"JIT directive output should NOT contain AOT definitions. Got:\n{}",
6187+
result.code
6188+
);
6189+
6190+
insta::assert_snapshot!("jit_directive", result.code);
6191+
}
6192+
6193+
#[test]
6194+
fn test_jit_full_component_example() {
6195+
// Full example matching the issue #97 scenario
6196+
let allocator = Allocator::default();
6197+
let source = r#"
6198+
import { Component, signal } from '@angular/core';
6199+
import { RouterOutlet } from '@angular/router';
6200+
import { Lib1 } from 'lib1';
6201+
import { TitleService } from './title.service';
6202+
6203+
@Component({
6204+
selector: 'app-root',
6205+
imports: [RouterOutlet, Lib1],
6206+
templateUrl: './app.html',
6207+
styleUrl: './app.css',
6208+
})
6209+
export class App {
6210+
titleService;
6211+
title = signal('app');
6212+
constructor(titleService: TitleService) {
6213+
this.titleService = titleService;
6214+
this.title.set(this.titleService.getTitle());
6215+
}
6216+
}
6217+
"#;
6218+
6219+
let options = ComponentTransformOptions { jit: true, ..Default::default() };
6220+
let result = transform_angular_file(&allocator, "app.component.ts", source, &options, None);
6221+
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);
6222+
6223+
// Should have all JIT characteristics
6224+
assert!(
6225+
result.code.contains("import { __decorate } from \"tslib\""),
6226+
"Missing __decorate import"
6227+
);
6228+
assert!(
6229+
result.code.contains("angular:jit:template:file;./app.html"),
6230+
"Missing template resource import"
6231+
);
6232+
assert!(
6233+
result.code.contains("angular:jit:style:file;./app.css"),
6234+
"Missing style resource import"
6235+
);
6236+
assert!(result.code.contains("let App = class App"), "Missing class restructuring");
6237+
assert!(result.code.contains("ctorParameters"), "Missing ctorParameters");
6238+
assert!(result.code.contains("__decorate("), "Missing __decorate call");
6239+
assert!(result.code.contains("export { App }"), "Missing named export");
6240+
6241+
// Should NOT have AOT output
6242+
assert!(!result.code.contains("ɵcmp"), "Should not contain ɵcmp");
6243+
assert!(!result.code.contains("ɵfac"), "Should not contain ɵfac");
6244+
assert!(!result.code.contains("defineComponent"), "Should not contain defineComponent");
6245+
6246+
insta::assert_snapshot!("jit_full_component", result.code);
6247+
}

0 commit comments

Comments
 (0)