Skip to content
Open
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
17 changes: 15 additions & 2 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,8 @@ struct JitClassInfo {
is_exported: bool,
/// Whether the class is export default.
is_default_export: bool,
/// Whether the class is abstract.
is_abstract: bool,
/// Constructor parameter info for ctorParameters.
ctor_params: std::vec::Vec<JitCtorParam>,
/// Member decorator info for propDecorators.
Expand Down Expand Up @@ -1224,6 +1226,7 @@ fn transform_angular_file_jit(
class_body_end: class.body.span.end,
is_exported,
is_default_export,
is_abstract: class.r#abstract,
ctor_params,
member_decorators,
decorator_text,
Expand Down Expand Up @@ -1343,15 +1346,25 @@ fn transform_angular_file_jit(
}

// 4c. Class restructuring: `export class X` → `let X = class X`
// For abstract classes, also strip the `abstract` keyword since class expressions can't be abstract.
let class_keyword_start = if jit_info.is_abstract {
let rest = &source[jit_info.class_start as usize..];
let offset = rest.find("class").unwrap_or(0);
jit_info.class_start + offset as u32
} else {
jit_info.class_start
};

if jit_info.is_exported || jit_info.is_default_export {
edits.push(Edit::replace(
jit_info.stmt_start,
jit_info.class_start,
class_keyword_start,
format!("let {} = ", jit_info.class_name),
));
} else {
edits.push(Edit::insert(
edits.push(Edit::replace(
jit_info.class_start,
class_keyword_start,
format!("let {} = ", jit_info.class_name),
));
}
Expand Down
46 changes: 46 additions & 0 deletions crates/oxc_angular_compiler/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6406,6 +6406,52 @@ export class TestComponent {
insta::assert_snapshot!("jit_union_type_ctor_params", result.code);
}

#[test]
fn test_jit_abstract_class() {
let allocator = Allocator::default();
let source = r#"
import { Injectable } from '@angular/core';

@Injectable()
export abstract class BaseProvider {
protected abstract get name(): string;
protected abstract initialize(): void;

public greet(): string {
return `Hello from ${this.name}`;
}
}
"#;

let options = ComponentTransformOptions { jit: true, ..Default::default() };
let result = transform_angular_file(&allocator, "base.provider.ts", source, &options, None);
assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics);

// The abstract keyword should NOT appear before "class" in the output
// (JIT converts to class expression which can't be abstract)
assert!(
!result.code.contains("abstract class"),
"JIT output should not contain 'abstract class'. Got:\n{}",
result.code
);

// Should have proper class expression
assert!(
result.code.contains("let BaseProvider = class BaseProvider"),
"JIT output should have class expression. Got:\n{}",
result.code
);

// Should have __decorate call
assert!(
result.code.contains("__decorate("),
"JIT output should use __decorate. Got:\n{}",
result.code
);

insta::assert_snapshot!("jit_abstract_class", result.code);
}

// =========================================================================
// Source map tests
// =========================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
assertion_line: 6452
expression: result.code
---

import { Injectable } from '@angular/core';
import { __decorate } from "tslib";

let BaseProvider = class BaseProvider {
protected abstract get name(): string;
protected abstract initialize(): void;

public greet(): string {
return `Hello from ${this.name}`;
}
};
BaseProvider = __decorate([
Injectable()
], BaseProvider);
export { BaseProvider };