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
306 changes: 288 additions & 18 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ oxc_parser = "0.122"
oxc_semantic = "0.122"
oxc_span = "0.122"
oxc_sourcemap = "6.0.1"
oxc_transformer = "0.122"
oxc_codegen = "0.122"

# Internal
oxc_angular_compiler = { path = "crates/oxc_angular_compiler" }
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_angular_compiler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ oxc_parser = { workspace = true }
oxc_semantic = { workspace = true }
oxc_span = { workspace = true }
oxc_sourcemap = { workspace = true }
oxc_transformer = { workspace = true }
oxc_codegen = { workspace = true }
miette = { workspace = true }
rustc-hash = { workspace = true }
indexmap = { workspace = true }
Expand Down
53 changes: 51 additions & 2 deletions crates/oxc_angular_compiler/src/component/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use std::collections::HashMap;

use std::path::Path;

use oxc_allocator::{Allocator, Vec as OxcVec};
use oxc_ast::ast::{
Argument, ArrayExpressionElement, Declaration, ExportDefaultDeclarationKind, Expression,
Expand Down Expand Up @@ -695,6 +697,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 @@ -1138,6 +1142,37 @@ fn build_jit_decorator_text(

/// Transform an Angular TypeScript file in JIT (Just-In-Time) compilation mode.
///
/// Strip TypeScript syntax from JIT output using oxc_transformer.
///
/// This runs as a post-pass after JIT text-edits, converting TypeScript → JavaScript.
/// It handles abstract members, type annotations, parameter properties, etc.
fn strip_typescript(allocator: &Allocator, path: &str, code: &str) -> String {
let source_type = SourceType::from_path(path).unwrap_or_default();
let parser_ret = Parser::new(allocator, code, source_type).parse();
if parser_ret.panicked {
return code.to_string();
}

let mut program = parser_ret.program;

let semantic_ret =
oxc_semantic::SemanticBuilder::new().with_excess_capacity(2.0).build(&program);

let ts_options =
oxc_transformer::TypeScriptOptions { only_remove_type_imports: true, ..Default::default() };

let transform_options =
oxc_transformer::TransformOptions { typescript: ts_options, ..Default::default() };

let transformer =
oxc_transformer::Transformer::new(allocator, Path::new(path), &transform_options);
transformer.build_with_scoping(semantic_ret.semantic.into_scoping(), &mut program);

let codegen_ret = oxc_codegen::Codegen::new().with_source_text(code).build(&program);

codegen_ret.code
}

/// JIT mode produces output compatible with Angular's JIT runtime compiler:
/// - Decorators are downleveled using `__decorate` from tslib
/// - `templateUrl` is replaced with `angular:jit:template:file;` imports
Expand Down Expand Up @@ -1224,6 +1259,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 +1379,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 Expand Up @@ -1395,6 +1441,9 @@ fn transform_angular_file_jit(
result.code = apply_edits(source, edits);
}

// 5. Strip TypeScript syntax from JIT output
result.code = strip_typescript(allocator, path, &result.code);

result
}

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,14 @@
---
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 {
greet() {
return `Hello from ${this.name}`;
}
};
BaseProvider = __decorate([Injectable()], BaseProvider);
export { BaseProvider };
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
assertion_line: 6201
expression: result.code
---
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { __decorate } from "tslib";

let AppComponent = class AppComponent {
title = 'app';
title = "app";
};
AppComponent = __decorate([
Component({
selector: 'app-root',
template: '<h1>Hello</h1>',
})
], AppComponent);
AppComponent = __decorate([Component({
selector: "app-root",
template: "<h1>Hello</h1>"
})], AppComponent);
export { AppComponent };
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
assertion_line: 6164
expression: result.code
---
import { Component } from '@angular/core';
import { TitleService } from './title.service';
import { Component } from "@angular/core";
import { TitleService } from "./title.service";
import { __decorate } from "tslib";

let AppComponent = class AppComponent {
constructor(private titleService: TitleService) {}

static ctorParameters = () => [
{ type: TitleService }
];
constructor(titleService) {
this.titleService = titleService;
}
static ctorParameters = () => [{ type: TitleService }];
};
AppComponent = __decorate([
Component({
selector: 'app-root',
template: '<h1>Hello</h1>',
})
], AppComponent);
AppComponent = __decorate([Component({
selector: "app-root",
template: "<h1>Hello</h1>"
})], AppComponent);
export { AppComponent };
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
assertion_line: 6239
expression: result.code
---
import { Directive, Input } from '@angular/core';
import { Directive, Input } from "@angular/core";
import { __decorate } from "tslib";

let HighlightDirective = class HighlightDirective {
color: string = 'yellow';

static propDecorators = {
color: [{ type: Input }]
color = "yellow";
static propDecorators = { color: [{ type: Input }] };
};
};
HighlightDirective = __decorate([
Directive({
selector: '[appHighlight]',
standalone: true,
})
], HighlightDirective);
HighlightDirective = __decorate([Directive({
selector: "[appHighlight]",
standalone: true
})], HighlightDirective);
export { HighlightDirective };
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
assertion_line: 6295
expression: result.code
---
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Lib1 } from 'lib1';
import { TitleService } from './title.service';
import { Component, signal } from "@angular/core";
import { RouterOutlet } from "@angular/router";
import { Lib1 } from "lib1";
import { TitleService } from "./title.service";
import { __decorate } from "tslib";
import __NG_CLI_RESOURCE__0 from "angular:jit:template:file;./app.html";
import __NG_CLI_RESOURCE__1 from "angular:jit:style:file;./app.css";

let App = class App {
titleService;
title = signal('app');
constructor(titleService: TitleService) {
this.titleService = titleService;
this.title.set(this.titleService.getTitle());
}

static ctorParameters = () => [
{ type: TitleService }
];
titleService;
title = signal("app");
constructor(titleService) {
this.titleService = titleService;
this.title.set(this.titleService.getTitle());
}
static ctorParameters = () => [{ type: TitleService }];
};
App = __decorate([
Component({
selector: 'app-root',
imports: [RouterOutlet, Lib1],
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1],
})
], App);
App = __decorate([Component({
selector: "app-root",
imports: [RouterOutlet, Lib1],
template: __NG_CLI_RESOURCE__0,
styles: [__NG_CLI_RESOURCE__1]
})], App);
export { App };
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
assertion_line: 6059
expression: result.code
---
import { Component } from '@angular/core';
import { Component } from "@angular/core";
import { __decorate } from "tslib";

let AppComponent = class AppComponent {};
AppComponent = __decorate([
Component({
selector: 'app-root',
template: '<h1>Hello</h1>',
standalone: true,
})
], AppComponent);
AppComponent = __decorate([Component({
selector: "app-root",
template: "<h1>Hello</h1>",
standalone: true
})], AppComponent);
export { AppComponent };
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
---
source: crates/oxc_angular_compiler/tests/integration_test.rs
assertion_line: 6351
expression: result.code
---
import { Directive, Input, Output, HostBinding, EventEmitter } from '@angular/core';
import { Directive, Input, Output, HostBinding, EventEmitter } from "@angular/core";
import { __decorate } from "tslib";

let HighlightDirective = class HighlightDirective {
color: string = 'yellow';
title: string = '';
colorChange = new EventEmitter<string>();
isActive = false;

static propDecorators = {
color: [{ type: Input }],
title: [{ type: Input, args: ['aliasName'] }],
colorChange: [{ type: Output }],
isActive: [{ type: HostBinding, args: ['class.active'] }]
color = "yellow";
title = "";
colorChange = new EventEmitter();
isActive = false;
static propDecorators = {
color: [{ type: Input }],
title: [{
type: Input,
args: ["aliasName"]
}],
colorChange: [{ type: Output }],
isActive: [{
type: HostBinding,
args: ["class.active"]
}]
};
};
};
HighlightDirective = __decorate([
Directive({
selector: '[appHighlight]',
})
], HighlightDirective);
HighlightDirective = __decorate([Directive({ selector: "[appHighlight]" })], HighlightDirective);
export { HighlightDirective };
Loading
Loading