1313//! |--------------------|-|
1414//! | `ɵɵngDeclareFactory` | Factory function |
1515//! | `ɵɵngDeclareInjectable` | `ɵɵdefineInjectable(...)` |
16+ //! | `ɵɵngDeclareService` | `ɵɵdefineService(...)` |
1617//! | `ɵɵngDeclareInjector` | `ɵɵdefineInjector(...)` |
1718//! | `ɵɵngDeclareNgModule` | `ɵɵdefineNgModule(...)` |
1819//! | `ɵɵngDeclarePipe` | `ɵɵdefinePipe(...)` |
@@ -58,6 +59,7 @@ fn quote_key(key: &str) -> String {
5859/// Partial declaration function names to link.
5960const DECLARE_FACTORY : & str = "\u{0275} \u{0275} ngDeclareFactory" ;
6061const DECLARE_INJECTABLE : & str = "\u{0275} \u{0275} ngDeclareInjectable" ;
62+ const DECLARE_SERVICE : & str = "\u{0275} \u{0275} ngDeclareService" ;
6163const DECLARE_INJECTOR : & str = "\u{0275} \u{0275} ngDeclareInjector" ;
6264const DECLARE_NG_MODULE : & str = "\u{0275} \u{0275} ngDeclareNgModule" ;
6365const DECLARE_PIPE : & str = "\u{0275} \u{0275} ngDeclarePipe" ;
@@ -350,6 +352,7 @@ fn get_declare_name<'a>(call: &'a CallExpression<'a>) -> Option<&'a str> {
350352 match name {
351353 DECLARE_FACTORY
352354 | DECLARE_INJECTABLE
355+ | DECLARE_SERVICE
353356 | DECLARE_INJECTOR
354357 | DECLARE_NG_MODULE
355358 | DECLARE_PIPE
@@ -361,8 +364,24 @@ fn get_declare_name<'a>(call: &'a CallExpression<'a>) -> Option<&'a str> {
361364 }
362365}
363366
364- /// Get the Angular import namespace (e.g., "i0") from the callee.
367+ /// Get the Angular import namespace (e.g., "i0") used to reference core symbols
368+ /// in the linked output.
369+ ///
370+ /// Prefers the declaration's own `ngImport` property, which is what the upstream
371+ /// TS linker uses (it emits `importExpr(R3.core)`, resolved via the file's import
372+ /// manager). This is important for bundles where a tool (e.g. esbuild's dep
373+ /// optimizer) has rewritten the `i0.ɵɵngDeclare*(...)` member call into a bare
374+ /// `ɵɵngDeclare*(...)` call while renaming the namespace import (e.g. to
375+ /// `core_exports`): the callee no longer carries the namespace, but `ngImport`
376+ /// still points at the correct alias. Falls back to the callee's object, then
377+ /// `i0`.
365378fn get_ng_import_namespace < ' a > ( call : & ' a CallExpression < ' a > ) -> & ' a str {
379+ if let Some ( meta) = get_metadata_object ( call)
380+ && let Some ( ns) = get_identifier_property ( meta, "ngImport" )
381+ {
382+ return ns;
383+ }
384+
366385 match & call. callee {
367386 Expression :: StaticMemberExpression ( member) => {
368387 if let Expression :: Identifier ( ident) = & member. object {
@@ -374,6 +393,21 @@ fn get_ng_import_namespace<'a>(call: &'a CallExpression<'a>) -> &'a str {
374393 }
375394}
376395
396+ /// Read an identifier-valued property (e.g. `ngImport: i0`) from an object.
397+ fn get_identifier_property < ' a > ( obj : & ' a ObjectExpression < ' a > , name : & str ) -> Option < & ' a str > {
398+ obj. properties . iter ( ) . find_map ( |prop| match prop {
399+ ObjectPropertyKind :: ObjectProperty ( p)
400+ if matches ! ( & p. key, PropertyKey :: StaticIdentifier ( ident) if ident. name == name) =>
401+ {
402+ match & p. value {
403+ Expression :: Identifier ( ident) => Some ( ident. name . as_str ( ) ) ,
404+ _ => None ,
405+ }
406+ }
407+ _ => None ,
408+ } )
409+ }
410+
377411/// Get the metadata object from a ɵɵngDeclare* call's first argument.
378412fn get_metadata_object < ' a > ( call : & ' a CallExpression < ' a > ) -> Option < & ' a ObjectExpression < ' a > > {
379413 call. arguments . first ( ) . and_then ( |arg| {
@@ -577,6 +611,17 @@ fn is_property_null(obj: &ObjectExpression<'_>, name: &str) -> bool {
577611 } )
578612}
579613
614+ /// Check if a property exists and its value is the boolean literal `false`.
615+ fn is_property_false ( obj : & ObjectExpression < ' _ > , name : & str ) -> bool {
616+ obj. properties . iter ( ) . any ( |prop| {
617+ matches ! ( prop,
618+ ObjectPropertyKind :: ObjectProperty ( p)
619+ if matches!( & p. key, PropertyKey :: StaticIdentifier ( ident) if ident. name == name)
620+ && matches!( & p. value, Expression :: BooleanLiteral ( b) if !b. value)
621+ )
622+ } )
623+ }
624+
580625/// Check if a property exists and its value is a specific string literal.
581626fn is_property_string ( obj : & ObjectExpression < ' _ > , name : & str , value : & str ) -> bool {
582627 obj. properties . iter ( ) . any ( |prop| {
@@ -852,6 +897,7 @@ fn link_declaration(
852897 let replacement = match name {
853898 DECLARE_FACTORY => link_factory ( meta, source, ns, type_name) ,
854899 DECLARE_INJECTABLE => link_injectable ( meta, source, ns, type_name) ,
900+ DECLARE_SERVICE => link_service ( meta, source, ns, type_name) ,
855901 DECLARE_INJECTOR => link_injector ( meta, source, ns, type_name) ,
856902 DECLARE_NG_MODULE => link_ng_module ( meta, source, ns, type_name) ,
857903 DECLARE_PIPE => link_pipe ( meta, source, ns, type_name) ,
@@ -1018,6 +1064,38 @@ fn link_injectable(
10181064 ) )
10191065}
10201066
1067+ /// Link ɵɵngDeclareService → ɵɵdefineService.
1068+ ///
1069+ /// `@Service` (Angular v22+) ships partial `ɵɵngDeclareService` declarations in
1070+ /// precompiled libraries (e.g. `@angular/common`'s `NgLocalization`). Mirrors the
1071+ /// TS linker's `PartialServiceLinkerVersion1` + `compileService`:
1072+ ///
1073+ /// - No `factory` field → delegate to the class factory: `{Type}.ɵfac`.
1074+ /// - `factory` field → wrap in an arrow that calls it: `() => (factory)()`.
1075+ /// - `autoProvided: false` is the only `autoProvided` value ever emitted (the
1076+ /// partial compiler omits it otherwise).
1077+ fn link_service (
1078+ meta : & ObjectExpression < ' _ > ,
1079+ source : & str ,
1080+ ns : & str ,
1081+ type_name : & str ,
1082+ ) -> Option < String > {
1083+ let factory = match get_property_source ( meta, "factory" , source) {
1084+ // `factory: () => (userFactory)()` — wrap the supplied factory.
1085+ Some ( user_factory) => format ! ( "() => ({user_factory})()" ) ,
1086+ // No factory supplied — delegate to the class's own ɵfac.
1087+ None => format ! ( "{type_name}.\u{0275} fac" ) ,
1088+ } ;
1089+
1090+ // Only `autoProvided: false` is ever present in the declaration.
1091+ let auto_provided_suffix =
1092+ if is_property_false ( meta, "autoProvided" ) { ", autoProvided: false" } else { "" } ;
1093+
1094+ Some ( format ! (
1095+ "{ns}.\u{0275} \u{0275} defineService({{ token: {type_name}, factory: {factory}{auto_provided_suffix} }})"
1096+ ) )
1097+ }
1098+
10211099/// Link ɵɵngDeclareInjector → ɵɵdefineInjector.
10221100fn link_injector (
10231101 meta : & ObjectExpression < ' _ > ,
@@ -2266,6 +2344,47 @@ MyService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
22662344 assert ! ( !result. code. contains( "ɵɵngDeclareInjectable" ) ) ;
22672345 }
22682346
2347+ #[ test]
2348+ fn test_link_service_delegates_to_fac ( ) {
2349+ let allocator = Allocator :: default ( ) ;
2350+ let code = r#"
2351+ import * as i0 from "@angular/core";
2352+ class MyService {
2353+ }
2354+ MyService.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.0", ngImport: i0, type: MyService });
2355+ "# ;
2356+ let result = link ( & allocator, code, "test.mjs" ) ;
2357+ assert ! ( result. linked) ;
2358+ assert ! (
2359+ result
2360+ . code
2361+ . contains( "i0.ɵɵdefineService({ token: MyService, factory: MyService.ɵfac })" )
2362+ ) ;
2363+ assert ! ( !result. code. contains( "ɵɵngDeclareService" ) ) ;
2364+ }
2365+
2366+ #[ test]
2367+ fn test_link_service_custom_factory_and_auto_provided ( ) {
2368+ let allocator = Allocator :: default ( ) ;
2369+ let code = r#"
2370+ import * as i0 from "@angular/core";
2371+ class NgLocalization {
2372+ }
2373+ NgLocalization.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.0", ngImport: i0, type: NgLocalization, autoProvided: false, factory: () => new NgLocaleLocalization(inject(LOCALE_ID)) });
2374+ "# ;
2375+ let result = link ( & allocator, code, "test.mjs" ) ;
2376+ assert ! ( result. linked) ;
2377+ // Custom factory is wrapped in an arrow that invokes it.
2378+ assert ! (
2379+ result
2380+ . code
2381+ . contains( "factory: () => (() => new NgLocaleLocalization(inject(LOCALE_ID)))()" )
2382+ ) ;
2383+ // autoProvided: false is preserved.
2384+ assert ! ( result. code. contains( "autoProvided: false" ) ) ;
2385+ assert ! ( !result. code. contains( "ɵɵngDeclareService" ) ) ;
2386+ }
2387+
22692388 #[ test]
22702389 fn test_link_class_metadata ( ) {
22712390 let allocator = Allocator :: default ( ) ;
0 commit comments