Skip to content

Commit 3e84e1c

Browse files
Brooooooklynclaude
andauthored
feat: generate .d.ts Ivy type declarations for Angular library builds (#101)
* feat: generate .d.ts Ivy type declarations for Angular library builds Add `dts_declarations` field to `TransformResult` that generates the static type declarations (ɵfac, ɵcmp, ɵdir, ɵpipe, ɵmod, ɵinj, ɵprov) needed in `.d.ts` files for Angular library consumers to perform template type-checking. This enables build tools like tsdown to post-process `.d.ts` output and inject Ivy declarations, solving the "Component imports must be standalone" error when publishing Angular libraries compiled with Oxc. Supports all Angular decorator types: @component, @directive, @pipe, @NgModule, and @Injectable, including input/output maps, signal inputs, exportAs, host directives, and constructor @Attribute() dependencies. - Close #86 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: align .d.ts declarations with Angular TS compiler - Fix factory CtorDeps to emit object literal types with attribute/optional/host/self/skipSelf - Emit `null` instead of `""` for pipe names when not specified - Add type_argument_count support for generic Injectable/Pipe/NgModule - Handle control character escaping (\n, \r, \t) in dts strings - Surface ng-content selectors from template compilation pipeline - Generate ngAcceptInputType_* fields for inputs with transforms - Add comprehensive tests for all dts generation scenarios Fix #86 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: improve host directive name extraction and deduplicate ctor deps - Handle ReadProp (namespace-qualified, e.g. i1.SomeDirective) and External expression variants in extract_directive_name_from_expr - Replace silent "unknown" fallback with descriptive panic for unhandled variants - Extract shared ctor deps formatting logic into generate_ctor_deps_type helper, reducing ~80 lines of duplication Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: populate directive type_argument_count and eliminate duplicate extract_injectable_metadata calls - Set type_argument_count on directive metadata from class.type_parameters, matching how Component/Pipe/NgModule/Injectable already do it. Without this, generic directives like `MyDirective<T>` emit `MyDirective` instead of `MyDirective<any>` in .d.ts declarations. - Eliminate 4 redundant extract_injectable_metadata calls (one per decorator branch) by capturing the result of the first call and deriving the boolean. - Add test for generic directive dts output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c380a55 commit 3e84e1c

File tree

6 files changed

+1821
-15
lines changed

6 files changed

+1821
-15
lines changed

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use crate::directive::{
3838
extract_content_queries, extract_directive_metadata, extract_view_queries,
3939
find_directive_decorator_span, generate_directive_definitions,
4040
};
41+
use crate::dts;
4142
use crate::injectable::{
4243
extract_injectable_metadata, find_injectable_decorator_span,
4344
generate_injectable_definition_from_decorator,
@@ -276,6 +277,20 @@ pub struct TransformResult {
276277

277278
/// Number of components found in the file.
278279
pub component_count: usize,
280+
281+
/// `.d.ts` type declarations for Angular classes.
282+
///
283+
/// Each entry contains the class name and the static member declarations
284+
/// that should be injected into the corresponding `.d.ts` class body.
285+
/// This enables library builds to include proper Ivy type declarations
286+
/// for template type-checking by consumers.
287+
///
288+
/// The declarations use `i0` as the namespace alias for `@angular/core`.
289+
/// Consumers must ensure their `.d.ts` files include:
290+
/// ```typescript
291+
/// import * as i0 from "@angular/core";
292+
/// ```
293+
pub dts_declarations: Vec<crate::dts::DtsDeclaration>,
279294
}
280295

281296
impl TransformResult {
@@ -1565,6 +1580,11 @@ pub fn transform_angular_file(
15651580
// Signal-based queries (contentChild(), contentChildren()) are also detected here
15661581
let content_queries = extract_content_queries(allocator, class);
15671582

1583+
// Collect content query property names for .d.ts generation
1584+
// (before content_queries is moved into compile_component_full)
1585+
let content_query_names: Vec<String> =
1586+
content_queries.iter().map(|q| q.property_name.to_string()).collect();
1587+
15681588
// 4. Compile the template and generate ɵcmp/ɵfac
15691589
// Pass the shared pool index to ensure unique constant names
15701590
// Pass the file-level namespace registry to ensure consistent namespace assignments
@@ -1617,15 +1637,14 @@ pub fn transform_angular_file(
16171637

16181638
// Check if the class also has an @Injectable decorator.
16191639
// @Injectable is SHARED precedence and can coexist with @Component.
1620-
if let Some(injectable_metadata) =
1621-
extract_injectable_metadata(allocator, class)
1622-
{
1640+
let has_injectable = extract_injectable_metadata(allocator, class);
1641+
if let Some(injectable_metadata) = &has_injectable {
16231642
if let Some(span) = find_injectable_decorator_span(class) {
16241643
decorator_spans_to_remove.push(span);
16251644
}
16261645
if let Some(inj_def) = generate_injectable_definition_from_decorator(
16271646
allocator,
1628-
&injectable_metadata,
1647+
injectable_metadata,
16291648
) {
16301649
let emitter = JsEmitter::new();
16311650
property_assignments.push_str(&format!(
@@ -1634,6 +1653,7 @@ pub fn transform_angular_file(
16341653
));
16351654
}
16361655
}
1656+
let has_injectable = has_injectable.is_some();
16371657

16381658
// Split declarations into two groups:
16391659
// 1. decls_before_class: child view functions, constants (needed BEFORE class)
@@ -1736,6 +1756,19 @@ pub fn transform_angular_file(
17361756
result.dependencies.push(style_url.to_string());
17371757
}
17381758

1759+
// Generate .d.ts type declaration for this component
1760+
let type_argument_count = class
1761+
.type_parameters
1762+
.as_ref()
1763+
.map_or(0, |tp| tp.params.len() as u32);
1764+
result.dts_declarations.push(dts::generate_component_dts(
1765+
&metadata,
1766+
type_argument_count,
1767+
&content_query_names,
1768+
has_injectable,
1769+
&compilation_result.ng_content_selectors,
1770+
));
1771+
17391772
result.component_count += 1;
17401773
}
17411774
Err(diags) => {
@@ -1814,21 +1847,30 @@ pub fn transform_angular_file(
18141847

18151848
// Check if the class also has an @Injectable decorator.
18161849
// @Injectable is SHARED precedence and can coexist with @Directive.
1817-
if let Some(injectable_metadata) = extract_injectable_metadata(allocator, class)
1818-
{
1850+
let has_injectable = extract_injectable_metadata(allocator, class);
1851+
if let Some(injectable_metadata) = &has_injectable {
18191852
if let Some(span) = find_injectable_decorator_span(class) {
18201853
decorator_spans_to_remove.push(span);
18211854
}
18221855
if let Some(inj_def) = generate_injectable_definition_from_decorator(
18231856
allocator,
1824-
&injectable_metadata,
1857+
injectable_metadata,
18251858
) {
18261859
property_assignments.push_str(&format!(
18271860
"\nstatic ɵprov = {};",
18281861
emitter.emit_expression(&inj_def.prov_definition)
18291862
));
18301863
}
18311864
}
1865+
let has_injectable = has_injectable.is_some();
1866+
1867+
// Generate .d.ts type declaration for this directive
1868+
let type_argument_count =
1869+
class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32);
1870+
directive_metadata.type_argument_count = type_argument_count;
1871+
result
1872+
.dts_declarations
1873+
.push(dts::generate_directive_dts(&directive_metadata, has_injectable));
18321874

18331875
class_positions.push((
18341876
class_name.clone(),
@@ -1879,22 +1921,31 @@ pub fn transform_angular_file(
18791921

18801922
// Check if the class also has an @Injectable decorator (issue #65).
18811923
// @Injectable is SHARED precedence and can coexist with @Pipe.
1882-
if let Some(injectable_metadata) =
1883-
extract_injectable_metadata(allocator, class)
1884-
{
1924+
let has_injectable = extract_injectable_metadata(allocator, class);
1925+
if let Some(injectable_metadata) = &has_injectable {
18851926
if let Some(span) = find_injectable_decorator_span(class) {
18861927
decorator_spans_to_remove.push(span);
18871928
}
18881929
if let Some(inj_def) = generate_injectable_definition_from_decorator(
18891930
allocator,
1890-
&injectable_metadata,
1931+
injectable_metadata,
18911932
) {
18921933
property_assignments.push_str(&format!(
18931934
"\nstatic ɵprov = {};",
18941935
emitter.emit_expression(&inj_def.prov_definition)
18951936
));
18961937
}
18971938
}
1939+
let has_injectable = has_injectable.is_some();
1940+
1941+
// Generate .d.ts type declaration for this pipe
1942+
let type_argument_count =
1943+
class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32);
1944+
result.dts_declarations.push(dts::generate_pipe_dts(
1945+
&pipe_metadata,
1946+
type_argument_count,
1947+
has_injectable,
1948+
));
18981949

18991950
class_positions.push((
19001951
class_name.clone(),
@@ -1951,22 +2002,22 @@ pub fn transform_angular_file(
19512002

19522003
// Check if the class also has an @Injectable decorator.
19532004
// @Injectable is SHARED precedence and can coexist with @NgModule.
1954-
if let Some(injectable_metadata) =
1955-
extract_injectable_metadata(allocator, class)
1956-
{
2005+
let has_injectable = extract_injectable_metadata(allocator, class);
2006+
if let Some(injectable_metadata) = &has_injectable {
19572007
if let Some(span) = find_injectable_decorator_span(class) {
19582008
decorator_spans_to_remove.push(span);
19592009
}
19602010
if let Some(inj_def) = generate_injectable_definition_from_decorator(
19612011
allocator,
1962-
&injectable_metadata,
2012+
injectable_metadata,
19632013
) {
19642014
property_assignments.push_str(&format!(
19652015
"\nstatic ɵprov = {};",
19662016
emitter.emit_expression(&inj_def.prov_definition)
19672017
));
19682018
}
19692019
}
2020+
let has_injectable = has_injectable.is_some();
19702021

19712022
// Collect any side-effect statements as external declarations
19722023
let mut external_decls = String::new();
@@ -1977,6 +2028,15 @@ pub fn transform_angular_file(
19772028
external_decls.push_str(&emitter.emit_statement(stmt));
19782029
}
19792030

2031+
// Generate .d.ts type declaration for this NgModule
2032+
let type_argument_count =
2033+
class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32);
2034+
result.dts_declarations.push(dts::generate_ng_module_dts(
2035+
&ng_module_metadata,
2036+
type_argument_count,
2037+
has_injectable,
2038+
));
2039+
19802040
// NgModule: external_decls go AFTER the class (they reference the class name)
19812041
class_positions.push((
19822042
class_name.clone(),
@@ -2028,6 +2088,14 @@ pub fn transform_angular_file(
20282088
emitter.emit_expression(&definition.prov_definition)
20292089
);
20302090

2091+
// Generate .d.ts type declaration for this injectable
2092+
let type_argument_count =
2093+
class.type_parameters.as_ref().map_or(0, |tp| tp.params.len() as u32);
2094+
result.dts_declarations.push(dts::generate_injectable_dts(
2095+
&injectable_metadata,
2096+
type_argument_count,
2097+
));
2098+
20312099
class_positions.push((
20322100
class_name.clone(),
20332101
compute_effective_start(class, &decorator_spans_to_remove, stmt_start),
@@ -2172,6 +2240,9 @@ struct FullCompilationResult {
21722240
/// The next constant pool index to use for the next component.
21732241
/// This is used to share pool state across multiple components in the same file.
21742242
next_pool_index: u32,
2243+
2244+
/// The ng-content selectors found in the template (e.g., `["*", ".header"]`).
2245+
ng_content_selectors: Vec<String>,
21752246
}
21762247

21772248
/// Compile a component template and generate ɵcmp/ɵfac definitions.
@@ -2246,6 +2317,10 @@ fn compile_component_full<'a>(
22462317
return Err(diagnostics);
22472318
}
22482319

2320+
// Capture ng-content selectors from the R3 AST for .d.ts generation
2321+
let ng_content_selectors: Vec<String> =
2322+
r3_result.ng_content_selectors.iter().map(|s| s.to_string()).collect();
2323+
22492324
// Merge inline template styles into component metadata
22502325
// These are styles from <style> tags directly in the template HTML
22512326
for style in r3_result.styles.iter() {
@@ -2486,6 +2561,7 @@ fn compile_component_full<'a>(
24862561
hmr_initializer_js,
24872562
class_debug_info_js,
24882563
next_pool_index,
2564+
ng_content_selectors,
24892565
})
24902566
}
24912567

0 commit comments

Comments
 (0)